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.

2812 lines
106 KiB

1 year ago
  1. /******/ (() => { // webpackBootstrap
  2. /******/ "use strict";
  3. var __webpack_exports__ = {};
  4. ;// CONCATENATED MODULE: ./src/constants.js
  5. const debouncedReload = foundry.utils.debounce(() => window.location.reload(), 100);
  6. const CONSTANTS = {
  7. MODULE_NAME: "pathmuncher",
  8. MODULE_FULL_NAME: "Pathmuncher",
  9. FLAG_NAME: "pathmuncher",
  10. SETTINGS: {
  11. // Enable options
  12. LOG_LEVEL: "log-level",
  13. RESTRICT_TO_TRUSTED: "restrict-to-trusted",
  14. ADD_VISION_FEATS: "add-vision-feats",
  15. },
  16. GET_DEFAULT_SETTINGS() {
  17. return foundry.utils.deepClone(CONSTANTS.DEFAULT_SETTINGS);
  18. },
  19. };
  20. CONSTANTS.DEFAULT_SETTINGS = {
  21. // Enable options
  22. [CONSTANTS.SETTINGS.RESTRICT_TO_TRUSTED]: {
  23. name: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Name`,
  24. hint: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Hint`,
  25. scope: "world",
  26. config: true,
  27. type: Boolean,
  28. default: false,
  29. onChange: debouncedReload,
  30. },
  31. [CONSTANTS.SETTINGS.ADD_VISION_FEATS]: {
  32. name: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Name`,
  33. hint: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Hint`,
  34. scope: "player",
  35. config: true,
  36. type: Boolean,
  37. default: true,
  38. },
  39. // debug
  40. [CONSTANTS.SETTINGS.LOG_LEVEL]: {
  41. name: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Name`,
  42. hint: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Hint`,
  43. scope: "world",
  44. config: true,
  45. type: String,
  46. choices: {
  47. DEBUG: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.debug`,
  48. INFO: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.info`,
  49. WARN: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.warn`,
  50. ERR: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.error`,
  51. OFF: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.off`,
  52. },
  53. default: "WARN",
  54. }
  55. };
  56. CONSTANTS.PATH = `modules/${CONSTANTS.MODULE_NAME}`;
  57. /* harmony default export */ const constants = (CONSTANTS);
  58. ;// CONCATENATED MODULE: ./src/data/equipment.js
  59. const SWAPS = [
  60. /^(Greater) (.*)/,
  61. /^(Lesser) (.*)/,
  62. /^(Major) (.*)/,
  63. /^(Moderate) (.*)/,
  64. /^(Standard) (.*)/,
  65. ];
  66. // this equipment is named differently in foundry vs pathbuilder
  67. const EQUIPMENT_RENAME_STATIC_MAP = [
  68. { pbName: "Chain", foundryName: "Chain (10 feet)" },
  69. { pbName: "Oil", foundryName: "Oil (1 pint)" },
  70. { pbName: "Bracelets of Dashing", foundryName: "Bracelet of Dashing" },
  71. { pbName: "Fingerprinting Kit", foundryName: "Fingerprint Kit" },
  72. { pbName: "Ladder", foundryName: "Ladder (10-foot)" },
  73. { pbName: "Mezmerizing Opal", foundryName: "Mesmerizing Opal" },
  74. { pbName: "Explorer's Clothing", foundryName: "Clothing (Explorer's)" },
  75. { pbName: "Flaming Star (Greater)", foundryName: "Greater Flaming Star" },
  76. { pbName: "Potion of Lesser Darkvision", foundryName: "Darkvision Elixir (Lesser)" },
  77. { pbName: "Potion of Greater Darkvision", foundryName: "Darkvision Elixir (Greater)" },
  78. { pbName: "Potion of Moderate Darkvision", foundryName: "Darkvision Elixir (Moderate)" },
  79. { pbName: "Bottled Sunlight", foundryName: "Formulated Sunlight" },
  80. { pbName: "Magazine (Repeating Hand Crossbow)", foundryName: "Magazine with 5 Bolts" },
  81. { pbName: "Astrolabe (Standard)", foundryName: "Standard Astrolabe" },
  82. { pbName: "Skinitch Salve", foundryName: "Skinstitch Salve" },
  83. { pbName: "Flawless Scale", foundryName: "Abadar's Flawless Scale" },
  84. { pbName: "Construct Key", foundryName: "Cordelia's Construct Key" },
  85. { pbName: "Construct Key (Greater)", foundryName: "Cordelia's Greater Construct Key" },
  86. { pbName: "Lesser Swapping Stone", foundryName: "Lesser Bonmuan Swapping Stone" },
  87. { pbName: "Major Swapping Stone", foundryName: "Major Bonmuan Swapping Stone" },
  88. { pbName: "Moderate Swapping Stone", foundryName: "Moderate Bonmuan Swapping Stone" },
  89. { pbName: "Greater Swapping Stone", foundryName: "Greater Bonmuan Swapping Stone" },
  90. { pbName: "Heartstone", foundryName: "Skarja's Heartstone" },
  91. { pbName: "Bullets (10 rounds)", foundryName: "Sling Bullets" },
  92. ];
  93. function generateDynamicNames(pbName) {
  94. const result = [];
  95. // if we have a hardcoded map, don't return here
  96. for (const reg of SWAPS) {
  97. const match = pbName.match(reg);
  98. if (match) {
  99. result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] });
  100. }
  101. }
  102. return result;
  103. }
  104. function EQUIPMENT_RENAME_MAP(pbName = null) {
  105. const postfixNames = pbName ? generateDynamicNames(pbName) : [];
  106. return postfixNames.concat(EQUIPMENT_RENAME_STATIC_MAP);
  107. }
  108. // this is equipment is special and shouldn't have the transformations applied to it
  109. const RESTRICTED_EQUIPMENT = [
  110. "Bracers of Armor",
  111. ];
  112. const IGNORED_EQUIPMENT = [
  113. "Unarmored"
  114. ];
  115. ;// CONCATENATED MODULE: ./src/utils.js
  116. const utils = {
  117. wait: async (ms) => {
  118. return new Promise((resolve) => {
  119. setTimeout(resolve, ms);
  120. });
  121. },
  122. capitalize: (s) => {
  123. if (typeof s !== "string") return "";
  124. return s.charAt(0).toUpperCase() + s.slice(1);
  125. },
  126. setting: (key) => {
  127. return game.settings.get(constants.MODULE_NAME, key);
  128. },
  129. updateSetting: async (key, value) => {
  130. return game.settings.set(constants.MODULE_NAME, key, value);
  131. },
  132. getFlags: (actor) => {
  133. const flags = actor.flags[constants.FLAG_NAME]
  134. ? actor.flags[constants.FLAG_NAME]
  135. : {
  136. pathbuilderId: undefined,
  137. addFeats: true,
  138. addEquipment: true,
  139. addBackground: true,
  140. addHeritage: true,
  141. addAncestry: true,
  142. addSpells: true,
  143. addMoney: true,
  144. addTreasure: true,
  145. addLores: true,
  146. addWeapons: true,
  147. addArmor: true,
  148. addDeity: true,
  149. addName: true,
  150. addClass: true,
  151. addFamiliars: true,
  152. addFormulas: true,
  153. askForChoices: false,
  154. };
  155. return flags;
  156. },
  157. setFlags: async (actor, flags) => {
  158. let updateData = {};
  159. setProperty(updateData, `flags.${constants.FLAG_NAME}`, flags);
  160. await actor.update(updateData);
  161. return actor;
  162. },
  163. resetFlags: async (actor) => {
  164. return utils.setFlags(actor, null);
  165. },
  166. getOrCreateFolder: async (root, entityType, folderName, folderColor = "") => {
  167. let folder = game.folders.contents.find((f) =>
  168. f.type === entityType && f.name === folderName
  169. // if a root folder we want to match the root id for the parent folder
  170. && (root ? root.id : null) === (f.folder?.id ?? null)
  171. );
  172. // console.warn(`Looking for ${root} ${entityType} ${folderName}`);
  173. // console.warn(folder);
  174. if (folder) return folder;
  175. folder = await Folder.create(
  176. {
  177. name: folderName,
  178. type: entityType,
  179. color: folderColor,
  180. parent: (root) ? root.id : null,
  181. },
  182. { displaySheet: false }
  183. );
  184. return folder;
  185. },
  186. // eslint-disable-next-line no-unused-vars
  187. getFolder: async (kind, subFolder = "", baseFolderName = "Pathmuncher", baseColor = "#6f0006", subColor = "#98020a", typeFolder = true) => {
  188. let entityTypes = new Map();
  189. entityTypes.set("pets", "Pets");
  190. const folderName = game.i18n.localize(`${constants.MODULE_NAME}.labels.${kind}`);
  191. const entityType = entityTypes.get(kind);
  192. const baseFolder = await utils.getOrCreateFolder(null, entityType, baseFolderName, baseColor);
  193. const entityFolder = typeFolder ? await utils.getOrCreateFolder(baseFolder, entityType, folderName, subColor) : baseFolder;
  194. if (subFolder !== "") {
  195. const subFolderName = subFolder.charAt(0).toUpperCase() + subFolder.slice(1);
  196. const typeFolder = await utils.getOrCreateFolder(entityFolder, entityType, subFolderName, subColor);
  197. return typeFolder;
  198. } else {
  199. return entityFolder;
  200. }
  201. },
  202. };
  203. /* harmony default export */ const src_utils = (utils);
  204. ;// CONCATENATED MODULE: ./src/data/features.js
  205. // these are features which are named differently in pathbuilder to foundry
  206. const POSTFIX_PB_REMOVALS = [
  207. /(.*) (Racket)$/,
  208. /(.*) (Style)$/,
  209. ];
  210. const PREFIX_PB_REMOVALS = [
  211. /^(Arcane Thesis): (.*)/,
  212. /^(Arcane School): (.*)/,
  213. /^(The) (.*)/,
  214. ];
  215. const PARENTHESIS = [
  216. /^(.*) \((.*)\)$/,
  217. ];
  218. const SPLITS = [
  219. /^(.*): (.*)/,
  220. ];
  221. const features_SWAPS = [
  222. /^(Greater) (.*)/,
  223. /^(Lesser) (.*)/,
  224. /^(Major) (.*)/,
  225. ];
  226. const FEAT_RENAME_STATIC_MAP = [
  227. { pbName: "Aerialist", foundryName: "Shory Aerialist" },
  228. { pbName: "Aeromancer", foundryName: "Shory Aeromancer" },
  229. { pbName: "Ancient-Blooded", foundryName: "Ancient-Blooded Dwarf" },
  230. { pbName: "Antipaladin [Chaotic Evil]", foundryName: "Antipaladin" },
  231. { pbName: "Ape", foundryName: "Ape Animal Instinct" },
  232. { pbName: "Aquatic Eyes (Darkvision)", foundryName: "Aquatic Eyes" },
  233. { pbName: "Astrology", foundryName: "Saoc Astrology" },
  234. { pbName: "Battle Ready", foundryName: "Battle-Ready Orc" },
  235. { pbName: "Bite (Gnoll)", foundryName: "Bite" },
  236. { pbName: "Bloodline: Genie (Efreeti)", foundryName: "Bloodline: Genie" },
  237. { pbName: "Bloody Debilitations", foundryName: "Bloody Debilitation" },
  238. { pbName: "Cave Climber Kobold", foundryName: "Caveclimber Kobold" },
  239. { pbName: "Chosen One", foundryName: "Chosen of Lamashtu" },
  240. { pbName: "Cognative Mutagen (Greater)", foundryName: "Cognitive Mutagen (Greater)" },
  241. { pbName: "Cognative Mutagen (Lesser)", foundryName: "Cognitive Mutagen (Lesser)" },
  242. { pbName: "Cognative Mutagen (Major)", foundryName: "Cognitive Mutagen (Major)" },
  243. { pbName: "Cognative Mutagen (Moderate)", foundryName: "Cognitive Mutagen (Moderate)" },
  244. { pbName: "Cognitive Crossover", foundryName: "Kreighton's Cognitive Crossover" },
  245. { pbName: "Collegiate Attendant Dedication", foundryName: "Magaambyan Attendant Dedication" },
  246. { pbName: "Construct Carver", foundryName: "Tupilaq Carver" },
  247. { pbName: "Constructed (Android)", foundryName: "Constructed" },
  248. { pbName: "Deadly Hair", foundryName: "Syu Tak-nwa's Deadly Hair" },
  249. { pbName: "Deepvision", foundryName: "Deep Vision" },
  250. { pbName: "Deflect Arrows", foundryName: "Deflect Arrow" },
  251. { pbName: "Desecrator [Neutral Evil]", foundryName: "Desecrator" },
  252. { pbName: "Detective Dedication", foundryName: "Edgewatch Detective Dedication" },
  253. { pbName: "Duelist Dedication (LO)", foundryName: "Aldori Duelist Dedication" },
  254. { pbName: "Dwarven Hold Education", foundryName: "Dongun Education" },
  255. { pbName: "Ember's Eyes (Darkvision)", foundryName: "Ember's Eyes" },
  256. { pbName: "Enhanced Familiar Feat", foundryName: "Enhanced Familiar" },
  257. { pbName: "Enigma", foundryName: "Enigma Muse" },
  258. { pbName: "Escape", foundryName: "Fane's Escape" },
  259. { pbName: "Eye of the Arcane Lords", foundryName: "Eye of the Arclords" },
  260. { pbName: "Flip", foundryName: "Farabellus Flip" },
  261. { pbName: "Fourberie", foundryName: "Fane's Fourberie" },
  262. { pbName: "Ganzi Gaze (Low-Light Vision)", foundryName: "Ganzi Gaze" },
  263. { pbName: "Guild Agent Dedication", foundryName: "Pathfinder Agent Dedication" },
  264. { pbName: "Harmful Font", foundryName: "Divine Font" },
  265. { pbName: "Healing Font", foundryName: "Divine Font" },
  266. { pbName: "Heatwave", foundryName: "Heat Wave" },
  267. { pbName: "Heavenseeker Dedication", foundryName: "Jalmeri Heavenseeker Dedication" },
  268. { pbName: "Heir of the Astrologers", foundryName: "Heir of the Saoc" },
  269. { pbName: "High Killer Training", foundryName: "Vernai Training" },
  270. { pbName: "Ice-Witch", foundryName: "Irriseni Ice-Witch" },
  271. { pbName: "Impeccable Crafter", foundryName: "Impeccable Crafting" },
  272. { pbName: "Incredible Beastmaster's Companion", foundryName: "Incredible Beastmaster Companion" },
  273. { pbName: "Interrogation", foundryName: "Bolera's Interrogation" },
  274. { pbName: "Katana", foundryName: "Katana Weapon Familiarity" },
  275. { pbName: "Liberator [Chaotic Good]", foundryName: "Liberator" },
  276. { pbName: "Lumberjack Dedication", foundryName: "Turpin Rowe Lumberjack Dedication" },
  277. { pbName: "Maestro", foundryName: "Maestro Muse" },
  278. { pbName: "Major Lesson I", foundryName: "Major Lesson" },
  279. { pbName: "Major Lesson II", foundryName: "Major Lesson" },
  280. { pbName: "Major Lesson III", foundryName: "Major Lesson" },
  281. { pbName: "Mantis God's Grip", foundryName: "Achaekek's Grip" },
  282. { pbName: "Marked for Death", foundryName: "Mark for Death" },
  283. { pbName: "Miraculous Spells", foundryName: "Miraculous Spell" },
  284. { pbName: "Multifarious", foundryName: "Multifarious Muse" },
  285. { pbName: "Paladin [Lawful Good]", foundryName: "Paladin" },
  286. { pbName: "Parry", foundryName: "Aldori Parry" },
  287. { pbName: "Polymath", foundryName: "Polymath Muse" },
  288. { pbName: "Precise Debilitation", foundryName: "Precise Debilitations" },
  289. { pbName: "Quick Climber", foundryName: "Quick Climb" },
  290. { pbName: "Recognise Threat", foundryName: "Recognize Threat" },
  291. { pbName: "Redeemer [Neutral Good]", foundryName: "Redeemer" },
  292. { pbName: "Revivification Protocall", foundryName: "Revivification Protocol" },
  293. { pbName: "Riposte", foundryName: "Aldori Riposte" },
  294. { pbName: "Rkoan Arts", foundryName: "Rokoan Arts" },
  295. { pbName: "Saberteeth", foundryName: "Saber Teeth" },
  296. { pbName: "Scholarly Recollection", foundryName: "Uzunjati Recollection" },
  297. { pbName: "Scholarly Storytelling", foundryName: "Uzunjati Storytelling" },
  298. { pbName: "Secret Lesson", foundryName: "Janatimo's Lessons" },
  299. { pbName: "Sentry Dedication", foundryName: "Lastwall Sentry Dedication" },
  300. { pbName: "Stab and Snag", foundryName: "Stella's Stab and Snag" },
  301. { pbName: "Tenets of Evil", foundryName: "The Tenets of Evil" },
  302. { pbName: "Tenets of Good", foundryName: "The Tenets of Good" },
  303. { pbName: "Tongue of the Sun and Moon", foundryName: "Tongue of Sun and Moon" },
  304. { pbName: "Tribal Bond", foundryName: "Quah Bond" },
  305. { pbName: "Tyrant [Lawful Evil]", foundryName: "Tyrant" },
  306. { pbName: "Vestigal Wings", foundryName: "Vestigial Wings" },
  307. { pbName: "Virtue-Forged Tattooed", foundryName: "Virtue-Forged Tattoos" },
  308. { pbName: "Wakizashi", foundryName: "Wakizashi Weapon Familiarity" },
  309. { pbName: "Warden", foundryName: "Lastwall Warden" },
  310. { pbName: "Warrior", foundryName: "Warrior Muse" },
  311. { pbName: "Wary Eye", foundryName: "Eye of Ozem" },
  312. { pbName: "Wayfinder Resonance Infiltrator", foundryName: "Westyr's Wayfinder Repository" },
  313. { pbName: "Wind God's Fan", foundryName: "Wind God’s Fan" },
  314. { pbName: "Wind God’s Fan", foundryName: "Wind God's Fan" },
  315. { pbName: "Black", foundryName: "Black Dragon" },
  316. { pbName: "Brine", foundryName: "Brine Dragon" },
  317. { pbName: "Copper", foundryName: "Copper Dragon" },
  318. { pbName: "Blue", foundryName: "Blue Dragon" },
  319. { pbName: "Bronze", foundryName: "Bronze Dragon" },
  320. { pbName: "Cloud", foundryName: "Cloud Dragon" },
  321. { pbName: "Sky", foundryName: "Sky Dragon" },
  322. { pbName: "Brass", foundryName: "Brass Dragon" },
  323. { pbName: "Underworld", foundryName: "Underworld Dragon" },
  324. { pbName: "Crystal", foundryName: "Crystal Dragon" },
  325. { pbName: "Forest", foundryName: "Forest Dragon" },
  326. { pbName: "Green", foundryName: "Green Dragon" },
  327. { pbName: "Sea", foundryName: "Sea Dragon" },
  328. { pbName: "Silver", foundryName: "Silver Dragon" },
  329. { pbName: "White", foundryName: "White Dragon" },
  330. { pbName: "Sovereign", foundryName: "Sovereign Dragon" },
  331. { pbName: "Umbral", foundryName: "Umbral Dragon" },
  332. { pbName: "Red", foundryName: "Red Dragon" },
  333. { pbName: "Gold", foundryName: "Gold Dragon" },
  334. { pbName: "Magma", foundryName: "Magma Dragon" },
  335. ];
  336. function features_generateDynamicNames(pbName) {
  337. const result = [];
  338. // if we have a hardcoded map, don't return here
  339. if (FEAT_RENAME_STATIC_MAP.some((e) => e.pbName === pbName)) return result;
  340. for (const reg of POSTFIX_PB_REMOVALS) {
  341. const match = pbName.match(reg);
  342. if (match) {
  343. result.push({ pbName, foundryName: match[1], details: match[2] });
  344. }
  345. }
  346. for (const reg of PREFIX_PB_REMOVALS) {
  347. const match = pbName.match(reg);
  348. if (match) {
  349. result.push({ pbName, foundryName: match[2], details: match[1] });
  350. }
  351. }
  352. for (const reg of SPLITS) {
  353. const match = pbName.match(reg);
  354. if (match) {
  355. result.push({ pbName, foundryName: match[2], details: match[1] });
  356. }
  357. }
  358. for (const reg of PARENTHESIS) {
  359. const match = pbName.match(reg);
  360. if (match) {
  361. result.push({ pbName, foundryName: match[1], details: match[2] });
  362. }
  363. }
  364. for (const reg of features_SWAPS) {
  365. const match = pbName.match(reg);
  366. if (match) {
  367. result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] });
  368. }
  369. }
  370. return result;
  371. }
  372. function FEAT_RENAME_MAP(pbName = null) {
  373. const postfixNames = pbName ? features_generateDynamicNames(pbName) : [];
  374. return postfixNames.concat(FEAT_RENAME_STATIC_MAP);
  375. }
  376. const IGNORED_FEATS_LIST = [
  377. "Unarmored",
  378. "Simple Weapon Expertise",
  379. "Spellbook",
  380. "Energy Emanation", // pathbuilder does not pass through a type for this
  381. "Imprecise Sense", // this gets picked up and added by granted features
  382. ];
  383. function IGNORED_FEATS() {
  384. const visionFeats = src_utils.setting(constants.SETTINGS.ADD_VISION_FEATS) ? [] : ["Low-Light Vision", "Darkvision"];
  385. return IGNORED_FEATS_LIST.concat(visionFeats);
  386. }
  387. ;// CONCATENATED MODULE: ./src/logger.js
  388. const logger = {
  389. _showMessage: (logLevel, data) => {
  390. if (!logLevel || !data || typeof logLevel !== "string") {
  391. return false;
  392. }
  393. const setting = src_utils.setting(constants.SETTINGS.LOG_LEVEL);
  394. const logLevels = ["DEBUG", "INFO", "WARN", "ERR", "OFF"];
  395. const logLevelIndex = logLevels.indexOf(logLevel.toUpperCase());
  396. if (setting == "OFF" || logLevelIndex === -1 || logLevelIndex < logLevels.indexOf(setting)) {
  397. return false;
  398. }
  399. return true;
  400. },
  401. log: (logLevel, ...data) => {
  402. if (!logger._showMessage(logLevel, data)) {
  403. return;
  404. }
  405. logLevel = logLevel.toUpperCase();
  406. let msg = "No logging message provided. Please see the payload for more information.";
  407. let payload = data.slice();
  408. if (data[0] && typeof (data[0] == "string")) {
  409. msg = data[0];
  410. if (data.length > 1) {
  411. payload = data.slice(1);
  412. } else {
  413. payload = null;
  414. }
  415. }
  416. msg = `${constants.MODULE_NAME} | ${logLevel} > ${msg}`;
  417. switch (logLevel) {
  418. case "DEBUG":
  419. if (payload) {
  420. console.debug(msg, ...payload); // eslint-disable-line no-console
  421. } else {
  422. console.debug(msg); // eslint-disable-line no-console
  423. }
  424. break;
  425. case "INFO":
  426. if (payload) {
  427. console.info(msg, ...payload); // eslint-disable-line no-console
  428. } else {
  429. console.info(msg); // eslint-disable-line no-console
  430. }
  431. break;
  432. case "WARN":
  433. if (payload) {
  434. console.warn(msg, ...payload); // eslint-disable-line no-console
  435. } else {
  436. console.warn(msg); // eslint-disable-line no-console
  437. }
  438. break;
  439. case "ERR":
  440. if (payload) {
  441. console.error(msg, ...payload); // eslint-disable-line no-console
  442. } else {
  443. console.error(msg); // eslint-disable-line no-console
  444. }
  445. break;
  446. default:
  447. break;
  448. }
  449. },
  450. debug: (...data) => {
  451. logger.log("DEBUG", ...data);
  452. },
  453. info: (...data) => {
  454. logger.log("INFO", ...data);
  455. },
  456. warn: (...data) => {
  457. logger.log("WARN", ...data);
  458. },
  459. error: (...data) => {
  460. logger.log("ERR", ...data);
  461. },
  462. };
  463. /* harmony default export */ const src_logger = (logger);
  464. ;// CONCATENATED MODULE: ./src/app/Pathmuncher.js
  465. /* eslint-disable no-await-in-loop */
  466. /* eslint-disable no-continue */
  467. class Pathmuncher {
  468. // eslint-disable-next-line class-methods-use-this
  469. EQUIPMENT_RENAME_MAP(name) {
  470. return EQUIPMENT_RENAME_MAP(name);
  471. }
  472. getFoundryEquipmentName(pbName) {
  473. return this.EQUIPMENT_RENAME_MAP(pbName).find((map) => map.pbName == pbName)?.foundryName ?? pbName;
  474. }
  475. FEAT_RENAME_MAP(name) {
  476. const dynamicItems = [
  477. { pbName: "Shining Oath", foundryName: `Shining Oath (${this.getChampionType()})` },
  478. { pbName: "Counterspell", foundryName: `Counterspell (${src_utils.capitalize(this.getClassSpellCastingType() ?? "")})` },
  479. { pbName: "Counterspell", foundryName: `Counterspell (${src_utils.capitalize(this.getClassSpellCastingType(true) ?? "")})` },
  480. { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${this.source.class})` },
  481. { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${this.source.dualClass})` },
  482. { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${src_utils.capitalize(this.getClassSpellCastingType() ?? "")} Caster)` },
  483. { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${src_utils.capitalize(this.getClassSpellCastingType(true) ?? "")} Caster)` },
  484. ];
  485. return FEAT_RENAME_MAP(name).concat(dynamicItems);
  486. }
  487. getFoundryFeatureName(pbName) {
  488. const match = this.FEAT_RENAME_MAP(pbName).find((map) => map.pbName == pbName);
  489. return match ?? { pbName, foundryName: pbName, details: undefined };
  490. }
  491. // eslint-disable-next-line class-methods-use-this
  492. get RESTRICTED_EQUIPMENT() {
  493. return RESTRICTED_EQUIPMENT;
  494. }
  495. // specials that are handled by Foundry and shouldn't be added
  496. // eslint-disable-next-line class-methods-use-this
  497. get IGNORED_FEATURES() {
  498. return IGNORED_FEATS();
  499. };
  500. // eslint-disable-next-line class-methods-use-this
  501. get IGNORED_EQUIPMENT() {
  502. return IGNORED_EQUIPMENT;
  503. };
  504. getChampionType() {
  505. if (this.source.alignment == "LG") return "Paladin";
  506. else if (this.source.alignment == "CG") return "Liberator";
  507. else if (this.source.alignment == "NG") return "Redeemer";
  508. else if (this.source.alignment == "LE") return "Tyrant";
  509. else if (this.source.alignment == "CE") return "Antipaladin";
  510. else if (this.source.alignment == "NE") return "Desecrator";
  511. return "Unknown";
  512. }
  513. constructor(actor, { addFeats = true, addEquipment = true, addSpells = true, addMoney = true, addLores = true,
  514. addWeapons = true, addArmor = true, addTreasure = true, addDeity = true, addName = true, addClass = true,
  515. addBackground = true, addHeritage = true, addAncestry = true, askForChoices = false } = {}
  516. ) {
  517. this.actor = actor;
  518. // note not all these options do anything yet!
  519. this.options = {
  520. addTreasure,
  521. addMoney,
  522. addFeats,
  523. addSpells,
  524. addEquipment,
  525. addLores,
  526. addWeapons,
  527. addArmor,
  528. addDeity,
  529. addName,
  530. addClass,
  531. addBackground,
  532. addHeritage,
  533. addAncestry,
  534. askForChoices,
  535. };
  536. this.source = null;
  537. this.parsed = {
  538. specials: [],
  539. feats: [],
  540. equipment: [],
  541. armor: [],
  542. weapons: [],
  543. };
  544. this.usedLocations = new Set();
  545. this.usedLocationsAlternateRules = new Set();
  546. this.autoAddedFeatureIds = new Set();
  547. this.autoAddedFeatureItems = {};
  548. this.allFeatureRules = {};
  549. this.autoAddedFeatureRules = {};
  550. this.grantItemLookUp = {};
  551. this.autoFeats = [];
  552. this.result = {
  553. character: {
  554. _id: this.actor.id,
  555. prototypeToken: {},
  556. },
  557. class: [],
  558. deity: [],
  559. heritage: [],
  560. ancestry: [],
  561. background: [],
  562. casters: [],
  563. spells: [],
  564. feats: [],
  565. weapons: [],
  566. armor: [],
  567. equipment: [],
  568. lores: [],
  569. money: [],
  570. treasure: [],
  571. adventurersPack: {
  572. item: null,
  573. contents: [
  574. { slug: "bedroll", qty: 1 },
  575. { slug: "chalk", qty: 10 },
  576. { slug: "flint-and-steel", qty: 1 },
  577. { slug: "rope", qty: 1 },
  578. { slug: "rations", qty: 14 },
  579. { slug: "torch", qty: 5 },
  580. { slug: "waterskin", qty: 1 },
  581. ],
  582. },
  583. focusPool: 0,
  584. };
  585. this.check = {};
  586. this.bad = [];
  587. }
  588. async fetchPathbuilder(pathbuilderId) {
  589. if (!pathbuilderId) {
  590. const flags = src_utils.getFlags(this.actor);
  591. pathbuilderId = flags?.pathbuilderId;
  592. }
  593. if (pathbuilderId) {
  594. const jsonData = await foundry.utils.fetchJsonWithTimeout(`https://www.pathbuilder2e.com/json.php?id=${pathbuilderId}`);
  595. if (jsonData.success) {
  596. this.source = jsonData.build;
  597. } else {
  598. ui.notifications.warn(game.i18n.format(`${constants.FLAG_NAME}.Dialogs.Pathmuncher.FetchFailed`, { pathbuilderId }));
  599. }
  600. } else {
  601. ui.notifications.error(game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.Pathmuncher.NoId`));
  602. }
  603. }
  604. getClassAdjustedSpecialNameLowerCase(name) {
  605. return `${name} (${this.source.class})`.toLowerCase();
  606. }
  607. getDualClassAdjustedSpecialNameLowerCase(name) {
  608. return `${name} (${this.source.dualClass})`.toLowerCase();
  609. }
  610. getAncestryAdjustedSpecialNameLowerCase(name) {
  611. return `${name} (${this.source.ancestry})`.toLowerCase();
  612. }
  613. getHeritageAdjustedSpecialNameLowerCase(name) {
  614. return `${name} (${this.source.heritage})`.toLowerCase();
  615. }
  616. static getMaterialGrade(material) {
  617. if (material.toLowerCase().includes("high-grade")) {
  618. return "high";
  619. } else if (material.toLowerCase().includes("standard-grade")) {
  620. return "standard";
  621. }
  622. return "low";
  623. }
  624. static getFoundryFeatLocation(pathbuilderFeatType, pathbuilderFeatLevel) {
  625. if (pathbuilderFeatType === "Ancestry Feat") {
  626. return `ancestry-${pathbuilderFeatLevel}`;
  627. } else if (pathbuilderFeatType === "Class Feat") {
  628. return `class-${pathbuilderFeatLevel}`;
  629. } else if (pathbuilderFeatType === "Skill Feat") {
  630. return `skill-${pathbuilderFeatLevel}`;
  631. } else if (pathbuilderFeatType === "General Feat") {
  632. return `general-${pathbuilderFeatLevel}`;
  633. } else if (pathbuilderFeatType === "Background Feat") {
  634. return `skill-${pathbuilderFeatLevel}`;
  635. } else if (pathbuilderFeatType === "Archetype Feat") {
  636. return `archetype-${pathbuilderFeatLevel}`;
  637. } else {
  638. return null;
  639. }
  640. }
  641. #generateFoundryFeatLocation(document, feature) {
  642. if (feature.type && feature.level) {
  643. const ancestryParagonVariant = game.settings.get("pf2e", "ancestryParagonVariant");
  644. const dualClassVariant = game.settings.get("pf2e", "dualClassVariant");
  645. // const freeArchetypeVariant = game.settings.get("pf2e", "freeArchetypeVariant");
  646. const location = Pathmuncher.getFoundryFeatLocation(feature.type, feature.level);
  647. if (location && !this.usedLocations.has(location)) {
  648. document.system.location = location;
  649. this.usedLocations.add(location);
  650. } else if (location && this.usedLocations.has(location)) {
  651. src_logger.debug("Variant feat location", { ancestryParagonVariant, location, feature });
  652. // eslint-disable-next-line max-depth
  653. if (ancestryParagonVariant && feature.type === "Ancestry Feat") {
  654. document.system.location = "ancestry-bonus";
  655. this.usedLocationsAlternateRules.add(location);
  656. } else if (dualClassVariant && feature.type === "Class Feat") {
  657. document.system.location = `dualclass-${feature.level}`;
  658. this.usedLocationsAlternateRules.add(location);
  659. }
  660. }
  661. }
  662. }
  663. #processSpecialData(name) {
  664. if (name.includes("Domain: ")) {
  665. const domainName = name.split(" ")[1];
  666. this.parsed.feats.push({ name: "Deity's Domain", extra: domainName });
  667. return true;
  668. } else {
  669. return false;
  670. }
  671. }
  672. #nameMap() {
  673. src_logger.debug("Starting Equipment Rename");
  674. this.source.equipment
  675. .filter((e) => e[0] && e[0] !== "undefined")
  676. .forEach((e) => {
  677. const name = this.getFoundryEquipmentName(e[0]);
  678. const item = { pbName: name, qty: e[1], added: false };
  679. this.parsed.equipment.push(item);
  680. });
  681. this.source.armor
  682. .filter((e) => e && e !== "undefined")
  683. .forEach((e) => {
  684. const name = this.getFoundryEquipmentName(e.name);
  685. const item = mergeObject({ pbName: name, originalName: e.name, added: false }, e);
  686. this.parsed.armor.push(item);
  687. });
  688. this.source.weapons
  689. .filter((e) => e && e !== "undefined")
  690. .forEach((e) => {
  691. const name = this.getFoundryEquipmentName(e.name);
  692. const item = mergeObject({ pbName: name, originalName: e.name, added: false }, e);
  693. this.parsed.weapons.push(item);
  694. });
  695. src_logger.debug("Finished Equipment Rename");
  696. src_logger.debug("Starting Special Rename");
  697. this.source.specials
  698. .filter((special) => special
  699. && special !== "undefined"
  700. && special !== "Not Selected"
  701. && special !== this.source.heritage
  702. )
  703. .forEach((special) => {
  704. const name = this.getFoundryFeatureName(special).foundryName;
  705. if (!this.#processSpecialData(name) && !this.IGNORED_FEATURES.includes(name)) {
  706. this.parsed.specials.push({ name, originalName: special, added: false });
  707. }
  708. });
  709. src_logger.debug("Finished Special Rename");
  710. src_logger.debug("Starting Feat Rename");
  711. this.source.feats
  712. .filter((feat) => feat[0]
  713. && feat[0] !== "undefined"
  714. && feat[0] !== "Not Selected"
  715. && feat[0] !== this.source.heritage
  716. )
  717. .forEach((feat) => {
  718. const name = this.getFoundryFeatureName(feat[0]).foundryName;
  719. const data = {
  720. name,
  721. extra: feat[1],
  722. added: false,
  723. type: feat[2],
  724. level: feat[3],
  725. originalName: feat[0],
  726. };
  727. this.parsed.feats.push(data);
  728. });
  729. src_logger.debug("Finished Feat Rename");
  730. }
  731. #prepare() {
  732. this.#nameMap();
  733. }
  734. static getSizeValue(size) {
  735. switch (size) {
  736. case 0:
  737. return "tiny";
  738. case 1:
  739. return "sm";
  740. case 3:
  741. return "lg";
  742. default:
  743. return "med";
  744. }
  745. }
  746. async #processSenses() {
  747. const senses = [];
  748. this.source.specials.forEach((special) => {
  749. if (special === "Low-Light Vision") {
  750. senses.push({ type: "lowLightVision" });
  751. } else if (special === "Darkvision") {
  752. senses.push({ type: "darkvision" });
  753. } else if (special === "Scent") {
  754. senses.push({ type: "scent" });
  755. }
  756. });
  757. setProperty(this.result.character, "system.traits.senses", senses);
  758. }
  759. async #addDualClass(klass) {
  760. if (!game.settings.get("pf2e", "dualClassVariant")) {
  761. if (this.source.dualClass && this.source.dualClass !== "") {
  762. src_logger.warn(`Imported character is dual class but system is not configured for dual class`, {
  763. class: this.source.class,
  764. dualClass: this.source.dualClass,
  765. });
  766. ui.notifications.warn(`Imported character is dual class but system is not configured for dual class`);
  767. }
  768. return;
  769. }
  770. if (!this.source.dualClass || this.source.dualClass === "") {
  771. src_logger.warn(`Imported character not dual class but system is configured for dual class`, {
  772. class: this.source.class,
  773. });
  774. ui.notifications.warn(`Imported character not dual class but system is configured for dual class`);
  775. return;
  776. }
  777. // find the dual class
  778. const compendium = await game.packs.get("pf2e.classes");
  779. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  780. const foundryName = this.getFoundryFeatureName(this.source.dualClass).foundryName;
  781. const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(foundryName))
  782. ?? index.find((i) => i.system.slug === game.pf2e.system.sluggify(this.source.dualClass));
  783. if (!indexMatch) return;
  784. const doc = await compendium.getDocument(indexMatch._id);
  785. const dualClass = doc.toObject();
  786. src_logger.debug(`Dual Class ${dualClass.name} found, squashing things together.`);
  787. klass.name = `${klass.name} - ${dualClass.name}`;
  788. const ruleEntry = {
  789. "domain": "all",
  790. "key": "RollOption",
  791. "option": `class:${dualClass.system.slug}`
  792. };
  793. // Attacks
  794. ["advanced", "martial", "simple", "unarmed"].forEach((key) => {
  795. if (dualClass.system.attacks[key] > klass.system.attacks[key]) {
  796. klass.system.attacks[key] = dualClass.system.attacks[key];
  797. }
  798. });
  799. if (klass.system.attacks.martial <= dualClass.system.attacks.other.rank) {
  800. if (dualClass.system.attacks.other.rank === klass.system.attacks.other.rank) {
  801. let mashed = `${klass.system.attacks.other.name}, ${dualClass.system.attacks.other.name}`;
  802. mashed = mashed.replace("and ", "");
  803. klass.system.attacks.other.name = [...new Set(mashed.split(','))].join(',');
  804. }
  805. if (dualClass.system.attacks.other.rank > klass.system.attacks.other.rank) {
  806. klass.system.attacks.other.name = dualClass.system.attacks.other.name;
  807. klass.system.attacks.other.rank = dualClass.system.attacks.other.rank;
  808. }
  809. }
  810. if (klass.system.attacks.martial >= dualClass.system.attacks.other.rank
  811. && klass.system.attacks.martial >= klass.system.attacks.other.rank
  812. ) {
  813. klass.system.attacks.other.rank = 0;
  814. klass.system.attacks.other.name = "";
  815. }
  816. // Class DC
  817. if (dualClass.system.classDC > klass.system.classDC) {
  818. klass.system.classDC = dualClass.system.classDC;
  819. }
  820. // Defenses
  821. ["heavy", "light", "medium", "unarmored"].forEach((key) => {
  822. if (dualClass.system.defenses[key] > klass.system.defenses[key]) {
  823. klass.system.defenses[key] = dualClass.system.defenses[key];
  824. }
  825. });
  826. // Description
  827. klass.system.description.value = `${klass.system.description.value} ${dualClass.system.description.value}`;
  828. // HP
  829. if (dualClass.system.hp > klass.system.hp) {
  830. klass.system.hp = dualClass.system.hp;
  831. }
  832. // Items
  833. Object.entries(dualClass.system.items).forEach((i) => {
  834. if (Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level > i[1].level)) {
  835. Object.values(klass.system.items).find((x) => x.uuid === i[1].uuid).level = i[1].level;
  836. } else if (!Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level <= i[1].level)) {
  837. klass.system.items[i[0]] = i[1];
  838. }
  839. });
  840. // Key Ability
  841. dualClass.system.keyAbility.value.forEach((v) => {
  842. if (!klass.system.keyAbility.value.includes(v)) {
  843. klass.system.keyAbility.value.push(v);
  844. }
  845. });
  846. // Perception
  847. if (dualClass.system.perception > klass.system.perception) klass.system.perception = dualClass.system.perception;
  848. // Rules
  849. klass.system.rules.push(ruleEntry);
  850. dualClass.system.rules.forEach((r) => {
  851. if (!klass.system.rules.includes(r)) {
  852. klass.system.rules.push(r);
  853. }
  854. });
  855. klass.system.rules.forEach((r, i) => {
  856. if (r.path !== undefined) {
  857. const check = r.path.split('.');
  858. if (check.includes("data")
  859. && check.includes("martial")
  860. && check.includes("rank")
  861. && klass.system.attacks.martial >= r.value
  862. ) {
  863. klass.system.rules.splice(i, 1);
  864. }
  865. }
  866. });
  867. // Saving Throws
  868. ["fortitude", "reflex", "will"].forEach((key) => {
  869. if (dualClass.system.savingThrows[key] > klass.system.savingThrows[key]) {
  870. klass.system.savingThrows[key] = dualClass.system.savingThrows[key];
  871. }
  872. });
  873. // Skill Feat Levels
  874. dualClass.system.skillFeatLevels.value.forEach((v) => {
  875. klass.system.skillFeatLevels.value.push(v);
  876. });
  877. klass.system.skillFeatLevels.value = [...new Set(klass.system.skillFeatLevels.value)].sort((a, b) => {
  878. return a - b;
  879. });
  880. // Skill Increase Levels
  881. dualClass.system.skillIncreaseLevels.value.forEach((v) => {
  882. klass.system.skillIncreaseLevels.value.push(v);
  883. });
  884. klass.system.skillIncreaseLevels.value = [...new Set(klass.system.skillIncreaseLevels.value)].sort((a, b) => {
  885. return a - b;
  886. });
  887. // Trained Skills
  888. if (dualClass.system.trainedSkills.additional > klass.system.trainedSkills.additional) {
  889. klass.system.trainedSkills.additional = dualClass.system.trainedSkills.additional;
  890. }
  891. dualClass.system.trainedSkills.value.forEach((v) => {
  892. if (!klass.system.trainedSkills.value.includes(v)) {
  893. klass.system.trainedSkills.value.push(v);
  894. }
  895. });
  896. this.result.dualClass = dualClass;
  897. }
  898. // eslint-disable-next-line class-methods-use-this
  899. async #processGenericCompendiumLookup(compendiumLabel, name, target) {
  900. src_logger.debug(`Checking for compendium documents for ${name} (${target}) in ${compendiumLabel}`);
  901. const compendium = await game.packs.get(compendiumLabel);
  902. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  903. const foundryName = this.getFoundryFeatureName(name).foundryName;
  904. const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(foundryName))
  905. ?? index.find((i) => i.system.slug === game.pf2e.system.sluggify(name));
  906. if (indexMatch) {
  907. const doc = await compendium.getDocument(indexMatch._id);
  908. const itemData = doc.toObject();
  909. if (target === "class") {
  910. itemData.system.keyAbility.selected = this.source.keyability;
  911. await this.#addDualClass(itemData);
  912. }
  913. itemData._id = foundry.utils.randomID();
  914. this.#generateGrantItemData(itemData);
  915. this.result[target].push(itemData);
  916. await this.#addGrantedItems(itemData);
  917. return true;
  918. } else {
  919. this.bad.push({ pbName: name, type: target, details: { name } });
  920. return false;
  921. }
  922. }
  923. // for grants, e.g. ont he champion "Deity and Cause" where there are choices.
  924. // how do we determine and match these? should we?
  925. // "pf2e": {
  926. // "itemGrants": {
  927. // "adanye": {
  928. // "id": "4GHcp3iaREfj2ZgN",
  929. // "onDelete": "detach"
  930. // },
  931. // "paladin": {
  932. // "id": "HGWkTEatliHgDaEu",
  933. // "onDelete": "detach"
  934. // }
  935. // }
  936. // }
  937. // "Paladin" (granted by deity and casue)
  938. // "pf2e": {
  939. // "grantedBy": {
  940. // "id": "xnrkrJa2YE1UOAVy",
  941. // "onDelete": "cascade"
  942. // },
  943. // "itemGrants": {
  944. // "retributiveStrike": {
  945. // "id": "WVHbj9LljCTovdsv",
  946. // "onDelete": "detach"
  947. // }
  948. // }
  949. // }
  950. // retributive strike
  951. // "pf2e": {
  952. // "grantedBy": {
  953. // "id": "HGWkTEatliHgDaEu",
  954. // "onDelete": "cascade"
  955. // }
  956. #parsedFeatureMatch(type, slug, ignoreAdded) {
  957. // console.warn(`Trying to find ${slug} in ${type}, ignoreAdded? ${ignoreAdded}`);
  958. const parsedMatch = this.parsed[type].find((f) =>
  959. (!ignoreAdded || (ignoreAdded && !f.added))
  960. && (
  961. slug === game.pf2e.system.sluggify(f.name)
  962. || slug === game.pf2e.system.sluggify(this.getClassAdjustedSpecialNameLowerCase(f.name))
  963. || slug === game.pf2e.system.sluggify(this.getAncestryAdjustedSpecialNameLowerCase(f.name))
  964. || slug === game.pf2e.system.sluggify(this.getHeritageAdjustedSpecialNameLowerCase(f.name))
  965. || slug === game.pf2e.system.sluggify(f.originalName)
  966. || slug === game.pf2e.system.sluggify(this.getClassAdjustedSpecialNameLowerCase(f.originalName))
  967. || slug === game.pf2e.system.sluggify(this.getAncestryAdjustedSpecialNameLowerCase(f.originalName))
  968. || slug === game.pf2e.system.sluggify(this.getHeritageAdjustedSpecialNameLowerCase(f.originalName))
  969. || (game.settings.get("pf2e", "dualClassVariant")
  970. && (slug === game.pf2e.system.sluggify(this.getDualClassAdjustedSpecialNameLowerCase(f.name))
  971. || slug === game.pf2e.system.sluggify(this.getDualClassAdjustedSpecialNameLowerCase(f.originalName))
  972. )
  973. )
  974. )
  975. );
  976. // console.warn(`Results of find ${slug} in ${type}, ignoreAdded? ${ignoreAdded}`, {
  977. // slug,
  978. // parsedMatch,
  979. // parsed: duplicate(this.parsed),
  980. // });
  981. return parsedMatch;
  982. }
  983. #generatedResultMatch(type, slug) {
  984. const featMatch = this.result[type].find((f) => slug === f.system.slug);
  985. return featMatch;
  986. }
  987. #findAllFeatureMatch(slug, ignoreAdded) {
  988. const featMatch = this.#parsedFeatureMatch("feats", slug, ignoreAdded);
  989. if (featMatch) return featMatch;
  990. const specialMatch = this.#parsedFeatureMatch("specials", slug, ignoreAdded);
  991. if (specialMatch) return specialMatch;
  992. const deityMatch = this.#generatedResultMatch("deity", slug);
  993. return deityMatch;
  994. // const classMatch = this.#generatedResultMatch("class", slug);
  995. // return classMatch;
  996. // const equipmentMatch = this.#generatedResultMatch("equipment", slug);
  997. // return equipmentMatch;
  998. }
  999. #createGrantedItem(document, parent) {
  1000. src_logger.debug(`Adding granted item flags to ${document.name} (parent ${parent.name})`);
  1001. const camelCase = game.pf2e.system.sluggify(document.system.slug, { camel: "dromedary" });
  1002. setProperty(parent, `flags.pf2e.itemGrants.${camelCase}`, { id: document._id, onDelete: "detach" });
  1003. setProperty(document, "flags.pf2e.grantedBy", { id: parent._id, onDelete: "cascade" });
  1004. this.autoFeats.push(document);
  1005. if (!this.options.askForChoices) {
  1006. this.result.feats.push(document);
  1007. }
  1008. const featureMatch = this.#findAllFeatureMatch(document.system.slug, true)
  1009. ?? (document.name.includes("(")
  1010. ? this.#findAllFeatureMatch(game.pf2e.system.sluggify(document.name.split("(")[0].trim()), true)
  1011. : undefined
  1012. );
  1013. // console.warn(`Matching feature for ${document.name}?`, {
  1014. // featureMatch,
  1015. // });
  1016. if (featureMatch) {
  1017. if (hasProperty(featureMatch, "added")) {
  1018. featureMatch.added = true;
  1019. this.#generateFoundryFeatLocation(document, featureMatch);
  1020. }
  1021. return;
  1022. }
  1023. if (document.type !== "action") src_logger.warn(`Unable to find parsed feature match for granted feature ${document.name}. This might not be an issue, but might indicate feature duplication.`, { document, parent });
  1024. }
  1025. async #featureChoiceMatch(choices, ignoreAdded, adjustName) {
  1026. for (const choice of choices) {
  1027. const doc = adjustName
  1028. ? game.i18n.localize(choice.label)
  1029. : await fromUuid(choice.value);
  1030. if (!doc) continue;
  1031. const slug = adjustName
  1032. ? game.pf2e.system.sluggify(doc)
  1033. : doc.system.slug;
  1034. const featMatch = this.#findAllFeatureMatch(slug, ignoreAdded);
  1035. if (featMatch) {
  1036. if (adjustName && hasProperty(featMatch, "added")) featMatch.added = true;
  1037. src_logger.debug("Choices evaluated", { choices, document, featMatch, choice });
  1038. return choice;
  1039. }
  1040. }
  1041. return undefined;
  1042. }
  1043. async #evaluateChoices(document, choiceSet) {
  1044. src_logger.debug(`Evaluating choices for ${document.name}`, { document, choiceSet });
  1045. const tempActor = await this.#generateTempActor();
  1046. const cleansedChoiceSet = deepClone(choiceSet);
  1047. try {
  1048. const item = tempActor.getEmbeddedDocument("Item", document._id);
  1049. const choiceSetRules = new game.pf2e.RuleElements.all.ChoiceSet(cleansedChoiceSet, item);
  1050. const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat();
  1051. const choices = (await choiceSetRules.inflateChoices()).filter((c) => !c.predicate || c.predicate.test(rollOptions));
  1052. src_logger.debug("Starting choice evaluation", {
  1053. document,
  1054. choiceSet,
  1055. item,
  1056. choiceSetRules,
  1057. rollOptions,
  1058. choices,
  1059. });
  1060. src_logger.debug("Evaluating choiceset", cleansedChoiceSet);
  1061. const choiceMatch = await this.#featureChoiceMatch(choices, true, cleansedChoiceSet.adjustName);
  1062. src_logger.debug("choiceMatch result", choiceMatch);
  1063. if (choiceMatch) return choiceMatch;
  1064. if (typeof cleansedChoiceSet.choices === "string" || Array.isArray(choices)) {
  1065. for (const choice of choices) {
  1066. const featMatch = this.#findAllFeatureMatch(choice.value, true, cleansedChoiceSet.adjustName);
  1067. if (featMatch) {
  1068. src_logger.debug("Choices evaluated", { cleansedChoiceSet, choices, document, featMatch, choice });
  1069. featMatch.added = true;
  1070. choice.nouuid = true;
  1071. return choice;
  1072. }
  1073. }
  1074. }
  1075. } catch (err) {
  1076. src_logger.error("Whoa! Something went major bad wrong during choice evaluation", {
  1077. err,
  1078. tempActor: tempActor.toObject(),
  1079. document: duplicate(document),
  1080. choiceSet: duplicate(cleansedChoiceSet),
  1081. });
  1082. throw err;
  1083. } finally {
  1084. await Actor.deleteDocuments([tempActor._id]);
  1085. }
  1086. src_logger.debug("Evaluate Choices failed", { choiceSet: cleansedChoiceSet, tempActor, document });
  1087. return undefined;
  1088. }
  1089. async #resolveInjectedUuid(source, propertyData) {
  1090. if (source === null || typeof source === "number" || (typeof source === "string" && !source.includes("{"))) {
  1091. return source;
  1092. }
  1093. // Walk the object tree and resolve any string values found
  1094. if (Array.isArray(source)) {
  1095. for (let i = 0; i < source.length; i++) {
  1096. source[i] = this.#resolveInjectedUuid(source[i]);
  1097. }
  1098. } else if (typeof source === 'object' && source !== null) {
  1099. for (const [key, value] of Object.entries(source)) {
  1100. if (typeof value === "string" || (typeof value === 'object' && value !== null)) {
  1101. source[key] = this.#resolveInjectedUuid(value);
  1102. }
  1103. }
  1104. return source;
  1105. } else if (typeof source === "string") {
  1106. const match = source.match(/{(actor|item|rule)\|(.*?)}/);
  1107. if (match && match[1] === "actor") {
  1108. return String(getProperty(this.result.character, match[1]));
  1109. } else if (match) {
  1110. const value = this.grantItemLookUp[match[0]].uuid;
  1111. if (!value) {
  1112. src_logger.error("Failed to resolve injected property", {
  1113. source,
  1114. propertyData,
  1115. key: match[1],
  1116. prop: match[2],
  1117. });
  1118. }
  1119. return String(value);
  1120. } else {
  1121. src_logger.error("Failed to resolve injected property", {
  1122. source,
  1123. propertyData,
  1124. });
  1125. }
  1126. }
  1127. return source;
  1128. }
  1129. async #generateGrantItemData(document) {
  1130. src_logger.debug(`Generating grantItem rule lookups for ${document.name}...`, { document: deepClone(document) });
  1131. for (const rule of document.system.rules.filter((r) => r.key === "GrantItem" && r.uuid.includes("{"))) {
  1132. src_logger.debug("Generating rule for...", { document: deepClone(document), rule });
  1133. const match = rule.uuid.match(/{(item|rule)\|(.*?)}/);
  1134. if (match) {
  1135. const flagName = match[2].split(".").pop();
  1136. const choiceSet = document.system.rules.find((rule) => rule.key === "ChoiceSet" && rule.flag === flagName)
  1137. ?? document.system.rules.find((rule) => rule.key === "ChoiceSet");
  1138. const choice = choiceSet ? (await this.#evaluateChoices(document, choiceSet)) : undefined;
  1139. const value = choice?.value ?? undefined;
  1140. if (!value) {
  1141. src_logger.warn("Failed to resolve injected uuid", {
  1142. ruleData: choiceSet,
  1143. flagName,
  1144. key: match[1],
  1145. prop: match[2],
  1146. value,
  1147. });
  1148. } else {
  1149. src_logger.debug(`Generated lookup ${value} for key ${document.name}`);
  1150. }
  1151. this.grantItemLookUp[rule.uuid] = {
  1152. docId: document.id,
  1153. key: rule.uuid,
  1154. choice,
  1155. uuid: value,
  1156. flag: flagName,
  1157. choiceSet,
  1158. };
  1159. this.grantItemLookUp[`${document._id}-${flagName}`] = {
  1160. docId: document.id,
  1161. key: rule.uuid,
  1162. choice,
  1163. uuid: value,
  1164. flag: flagName,
  1165. choiceSet,
  1166. };
  1167. this.grantItemLookUp[`${document._id}`] = {
  1168. docId: document.id,
  1169. key: rule.uuid,
  1170. choice,
  1171. uuid: value,
  1172. flag: flagName,
  1173. choiceSet,
  1174. };
  1175. this.grantItemLookUp[`${document._id}-${flagName}`] = {
  1176. docId: document.id,
  1177. key: rule.uuid,
  1178. choice,
  1179. uuid: value,
  1180. flag: flagName,
  1181. choiceSet,
  1182. };
  1183. } else {
  1184. src_logger.error("Failed to resolve injected uuid", {
  1185. document,
  1186. rule,
  1187. });
  1188. }
  1189. }
  1190. }
  1191. async #checkRule(document, rule) {
  1192. const tempActor = await this.#generateTempActor([document]);
  1193. const cleansedRule = deepClone(rule);
  1194. try {
  1195. const item = tempActor.getEmbeddedDocument("Item", document._id);
  1196. const ruleElement = cleansedRule.key === "ChoiceSet"
  1197. ? new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, item)
  1198. : new game.pf2e.RuleElements.all.GrantItem(cleansedRule, item);
  1199. const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat();
  1200. const choices = cleansedRule.key === "ChoiceSet"
  1201. ? (await ruleElement.inflateChoices()).filter((c) => !c.predicate || c.predicate.test(rollOptions))
  1202. : [ruleElement.resolveValue()];
  1203. const isGood = cleansedRule.key === "ChoiceSet"
  1204. ? (await this.#featureChoiceMatch(choices, false)) !== undefined
  1205. : ruleElement.test(rollOptions);
  1206. return isGood;
  1207. } catch (err) {
  1208. src_logger.error("Something has gone most wrong during rule checking", {
  1209. document,
  1210. rule: cleansedRule,
  1211. tempActor,
  1212. });
  1213. throw err;
  1214. } finally {
  1215. await Actor.deleteDocuments([tempActor._id]);
  1216. }
  1217. }
  1218. // eslint-disable-next-line complexity
  1219. async #addGrantedRules(document) {
  1220. if (document.system.rules.length === 0) return;
  1221. src_logger.debug(`addGrantedRules for ${document.name}`, duplicate(document));
  1222. if (hasProperty(document, "system.level.value")
  1223. && document.system.level.value > this.result.character.system.details.level.value
  1224. ) {
  1225. return;
  1226. }
  1227. // const rulesToKeep = document.system.rules.filter((r) => !["GrantItem", "ChoiceSet", "MartialProficiency"].includes(r.key));
  1228. const rulesToKeep = [];
  1229. this.allFeatureRules[document._id] = deepClone(document.system.rules);
  1230. this.autoAddedFeatureRules[document._id] = deepClone(document.system.rules.filter((r) => !["GrantItem", "ChoiceSet"].includes(r.key)));
  1231. await this.#generateGrantItemData(document);
  1232. const grantRules = document.system.rules.filter((r) => r.key === "GrantItem");
  1233. const choiceRules = document.system.rules.filter((r) => r.key === "ChoiceSet");
  1234. for (const ruleTypes of [choiceRules, grantRules]) {
  1235. for (const rawRuleEntry of ruleTypes) {
  1236. const ruleEntry = deepClone(rawRuleEntry);
  1237. src_logger.debug(`Checking ${document.name} rule key: ${ruleEntry.key}`);
  1238. const lookupName = ruleEntry.flag ? `${document._id}-${ruleEntry.flag}` : document._id;
  1239. src_logger.debug("Rule check, looking up", {
  1240. id: `${document._id}-${ruleEntry.flag}`,
  1241. lookup: this.grantItemLookUp[lookupName],
  1242. lookups: this.grantItemLookUp,
  1243. ruleEntry,
  1244. lookupName,
  1245. });
  1246. // have we pre-evaluated this choice?
  1247. const choice = ruleEntry.key === "ChoiceSet"
  1248. ? this.grantItemLookUp[lookupName]?.choice
  1249. ? this.grantItemLookUp[lookupName].choice
  1250. : await this.#evaluateChoices(document, ruleEntry)
  1251. : undefined;
  1252. const uuid = ruleEntry.key === "GrantItem"
  1253. ? await this.#resolveInjectedUuid(ruleEntry.uuid, ruleEntry)
  1254. : choice?.value;
  1255. src_logger.debug(`UUID for ${document.name}: "${uuid}"`, document, ruleEntry, choice);
  1256. const ruleFeature = uuid ? await fromUuid(uuid) : undefined;
  1257. if (ruleFeature) {
  1258. const featureDoc = ruleFeature.toObject();
  1259. featureDoc._id = foundry.utils.randomID();
  1260. if (featureDoc.system.rules) this.allFeatureRules[document._id] = deepClone(document.system.rules);
  1261. setProperty(featureDoc, "flags.pathmuncher.origin.uuid", uuid);
  1262. if (this.autoAddedFeatureIds.has(`${ruleFeature.id}${ruleFeature.type}`)) {
  1263. src_logger.debug(`Feature ${featureDoc.name} found for ${document.name}, but has already been added (${ruleFeature.id})`, ruleFeature);
  1264. continue;
  1265. }
  1266. src_logger.debug(`Found rule feature ${featureDoc.name} for ${document.name} for`, ruleEntry);
  1267. if (ruleEntry.predicate) {
  1268. const testResult = await this.#checkRule(featureDoc, ruleEntry);
  1269. // eslint-disable-next-line max-depth
  1270. if (!testResult) {
  1271. const data = { document, ruleEntry, featureDoc, testResult };
  1272. src_logger.debug(`The test failed for ${document.name} rule key: ${ruleFeature.key} (This is probably not a problem).`, data);
  1273. continue;
  1274. }
  1275. }
  1276. if (choice) {
  1277. ruleEntry.selection = choice.value;
  1278. }
  1279. this.autoAddedFeatureIds.add(`${ruleFeature.id}${ruleFeature.type}`);
  1280. featureDoc._id = foundry.utils.randomID();
  1281. this.#createGrantedItem(featureDoc, document);
  1282. if (hasProperty(ruleFeature, "system.rules.length")) await this.#addGrantedRules(featureDoc);
  1283. } else if (choice?.nouuid) {
  1284. src_logger.debug("Parsed no id rule", { choice, uuid, ruleEntry });
  1285. if (!ruleEntry.flag) ruleEntry.flag = game.pf2e.system.sluggify(document.name, { camel: "dromedary" });
  1286. ruleEntry.selection = choice.value;
  1287. if (choice.label) document.name = `${document.name} (${choice.label})`;
  1288. } else if (choice && uuid && !hasProperty(ruleEntry, "selection")) {
  1289. src_logger.debug("Parsed odd choice rule", { choice, uuid, ruleEntry });
  1290. if (!ruleEntry.flag) ruleEntry.flag = game.pf2e.system.sluggify(document.name, { camel: "dromedary" });
  1291. ruleEntry.selection = choice.value;
  1292. if (ruleEntry.adjustName && choice.label) {
  1293. const label = game.i18n.localize(choice.label);
  1294. const name = `${document.name} (${label})`;
  1295. const pattern = (() => {
  1296. const escaped = RegExp.escape(label);
  1297. return new RegExp(`\\(${escaped}\\) \\(${escaped}\\)$`);
  1298. })();
  1299. document.name = name.replace(pattern, `(${label})`);
  1300. }
  1301. } else {
  1302. const data = {
  1303. uuid: ruleEntry.uuid,
  1304. document,
  1305. ruleEntry,
  1306. choice,
  1307. lookup: this.grantItemLookUp[ruleEntry.uuid],
  1308. };
  1309. if (ruleEntry.key === "GrantItem" && this.grantItemLookUp[ruleEntry.uuid]) {
  1310. rulesToKeep.push(ruleEntry);
  1311. // const lookup = this.grantItemLookUp[ruleEntry.uuid].choiceSet
  1312. // eslint-disable-next-line max-depth
  1313. // if (!rulesToKeep.some((r) => r.key == lookup && r.prompt === lookup.prompt)) {
  1314. // rulesToKeep.push(this.grantItemLookUp[ruleEntry.uuid].choiceSet);
  1315. // }
  1316. } else if (ruleEntry.key === "ChoiceSet" && !hasProperty(ruleEntry, "flag")) {
  1317. src_logger.debug("Prompting user for choices", ruleEntry);
  1318. rulesToKeep.push(ruleEntry);
  1319. }
  1320. src_logger.warn("Unable to determine granted rule feature, needs better parser", data);
  1321. }
  1322. this.autoAddedFeatureRules[document._id].push(ruleEntry);
  1323. }
  1324. }
  1325. if (!this.options.askForChoices) {
  1326. // eslint-disable-next-line require-atomic-updates
  1327. document.system.rules = rulesToKeep;
  1328. }
  1329. }
  1330. async #addGrantedItems(document) {
  1331. if (hasProperty(document, "system.items")) {
  1332. src_logger.debug(`addGrantedItems for ${document.name}`, duplicate(document));
  1333. if (!this.autoAddedFeatureItems[document._id]) {
  1334. this.autoAddedFeatureItems[document._id] = duplicate(document.system.items);
  1335. }
  1336. const failedFeatureItems = {};
  1337. for (const [key, grantedItemFeature] of Object.entries(document.system.items)) {
  1338. src_logger.debug(`Checking granted item ${document.name}, with key: ${key}`, grantedItemFeature);
  1339. if (grantedItemFeature.level > getProperty(this.result.character, "system.details.level.value")) continue;
  1340. const feature = await fromUuid(grantedItemFeature.uuid);
  1341. if (!feature) {
  1342. const data = { uuid: grantedItemFeature.uuid, grantedFeature: grantedItemFeature, feature };
  1343. src_logger.warn("Unable to determine granted item feature, needs better parser", data);
  1344. failedFeatureItems[key] = grantedItemFeature;
  1345. continue;
  1346. }
  1347. this.autoAddedFeatureIds.add(`${feature.id}${feature.type}`);
  1348. const featureDoc = feature.toObject();
  1349. featureDoc._id = foundry.utils.randomID();
  1350. setProperty(featureDoc.system, "location", document._id);
  1351. this.#createGrantedItem(featureDoc, document);
  1352. if (hasProperty(featureDoc, "system.rules")) await this.#addGrantedRules(featureDoc);
  1353. }
  1354. if (!this.options.askForChoices) {
  1355. // eslint-disable-next-line require-atomic-updates
  1356. document.system.items = failedFeatureItems;
  1357. }
  1358. }
  1359. if (hasProperty(document, "system.rules")) await this.#addGrantedRules(document);
  1360. }
  1361. async #detectGrantedFeatures() {
  1362. if (this.result.class.length > 0) await this.#addGrantedItems(this.result.class[0]);
  1363. if (this.result.ancestry.length > 0) await this.#addGrantedItems(this.result.ancestry[0]);
  1364. if (this.result.heritage.length > 0) await this.#addGrantedItems(this.result.heritage[0]);
  1365. if (this.result.background.length > 0) await this.#addGrantedItems(this.result.background[0]);
  1366. }
  1367. async #processCore() {
  1368. setProperty(this.result.character, "name", this.source.name);
  1369. setProperty(this.result.character, "prototypeToken.name", this.source.name);
  1370. setProperty(this.result.character, "system.details.level.value", this.source.level);
  1371. if (this.source.age !== "Not set") setProperty(this.result.character, "system.details.age.value", this.source.age);
  1372. if (this.source.gender !== "Not set") setProperty(this.result.character, "system.details.gender.value", this.source.gender);
  1373. setProperty(this.result.character, "system.details.alignment.value", this.source.alignment);
  1374. setProperty(this.result.character, "system.details.keyability.value", this.source.keyability);
  1375. if (this.source.deity !== "Not set") setProperty(this.result.character, "system.details.deity.value", this.source.deity);
  1376. setProperty(this.result.character, "system.traits.size.value", Pathmuncher.getSizeValue(this.source.size));
  1377. setProperty(this.result.character, "system.traits.languages.value", this.source.languages.map((l) => l.toLowerCase()));
  1378. this.#processSenses();
  1379. setProperty(this.result.character, "system.abilities.str.value", this.source.abilities.str);
  1380. setProperty(this.result.character, "system.abilities.dex.value", this.source.abilities.dex);
  1381. setProperty(this.result.character, "system.abilities.con.value", this.source.abilities.con);
  1382. setProperty(this.result.character, "system.abilities.int.value", this.source.abilities.int);
  1383. setProperty(this.result.character, "system.abilities.wis.value", this.source.abilities.wis);
  1384. setProperty(this.result.character, "system.abilities.cha.value", this.source.abilities.cha);
  1385. setProperty(this.result.character, "system.saves.fortitude.tank", this.source.proficiencies.fortitude / 2);
  1386. setProperty(this.result.character, "system.saves.reflex.value", this.source.proficiencies.reflex / 2);
  1387. setProperty(this.result.character, "system.saves.will.value", this.source.proficiencies.will / 2);
  1388. setProperty(this.result.character, "system.martial.advanced.rank", this.source.proficiencies.advanced / 2);
  1389. setProperty(this.result.character, "system.martial.heavy.rank", this.source.proficiencies.heavy / 2);
  1390. setProperty(this.result.character, "system.martial.light.rank", this.source.proficiencies.light / 2);
  1391. setProperty(this.result.character, "system.martial.medium.rank", this.source.proficiencies.medium / 2);
  1392. setProperty(this.result.character, "system.martial.unarmored.rank", this.source.proficiencies.unarmored / 2);
  1393. setProperty(this.result.character, "system.martial.martial.rank", this.source.proficiencies.martial / 2);
  1394. setProperty(this.result.character, "system.martial.simple.rank", this.source.proficiencies.simple / 2);
  1395. setProperty(this.result.character, "system.martial.unarmed.rank", this.source.proficiencies.unarmed / 2);
  1396. setProperty(this.result.character, "system.skills.acr.rank", this.source.proficiencies.acrobatics / 2);
  1397. setProperty(this.result.character, "system.skills.arc.rank", this.source.proficiencies.arcana / 2);
  1398. setProperty(this.result.character, "system.skills.ath.rank", this.source.proficiencies.athletics / 2);
  1399. setProperty(this.result.character, "system.skills.cra.rank", this.source.proficiencies.crafting / 2);
  1400. setProperty(this.result.character, "system.skills.dec.rank", this.source.proficiencies.deception / 2);
  1401. setProperty(this.result.character, "system.skills.dip.rank", this.source.proficiencies.diplomacy / 2);
  1402. setProperty(this.result.character, "system.skills.itm.rank", this.source.proficiencies.intimidation / 2);
  1403. setProperty(this.result.character, "system.skills.med.rank", this.source.proficiencies.medicine / 2);
  1404. setProperty(this.result.character, "system.skills.nat.rank", this.source.proficiencies.nature / 2);
  1405. setProperty(this.result.character, "system.skills.occ.rank", this.source.proficiencies.occultism / 2);
  1406. setProperty(this.result.character, "system.skills.prf.rank", this.source.proficiencies.performance / 2);
  1407. setProperty(this.result.character, "system.skills.rel.rank", this.source.proficiencies.religion / 2);
  1408. setProperty(this.result.character, "system.skills.soc.rank", this.source.proficiencies.society / 2);
  1409. setProperty(this.result.character, "system.skills.ste.rank", this.source.proficiencies.stealth / 2);
  1410. setProperty(this.result.character, "system.skills.sur.rank", this.source.proficiencies.survival / 2);
  1411. setProperty(this.result.character, "system.skills.thi.rank", this.source.proficiencies.thievery / 2);
  1412. setProperty(this.result.character, "system.attributes.perception.rank", this.source.proficiencies.perception / 2);
  1413. setProperty(this.result.character, "system.attributes.classDC.rank", this.source.proficiencies.classDC / 2);
  1414. }
  1415. #indexFind(index, arrayOfNameMatches) {
  1416. for (const name of arrayOfNameMatches) {
  1417. const indexMatch = index.find((i) =>
  1418. i.system.slug === game.pf2e.system.sluggify(name)
  1419. || i.system.slug === game.pf2e.system.sluggify(this.getClassAdjustedSpecialNameLowerCase(name))
  1420. || i.system.slug === game.pf2e.system.sluggify(this.getAncestryAdjustedSpecialNameLowerCase(name))
  1421. || i.system.slug === game.pf2e.system.sluggify(this.getHeritageAdjustedSpecialNameLowerCase(name))
  1422. || (game.settings.get("pf2e", "dualClassVariant")
  1423. && (i.system.slug === game.pf2e.system.sluggify(this.getDualClassAdjustedSpecialNameLowerCase(name))
  1424. )
  1425. )
  1426. );
  1427. if (indexMatch) return indexMatch;
  1428. }
  1429. return undefined;
  1430. }
  1431. async #generateFeatItems(compendiumLabel) {
  1432. const compendium = await game.packs.get(compendiumLabel);
  1433. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  1434. this.parsed.feats.sort((f1, f2) => {
  1435. const f1RefUndefined = !(typeof f1.type === "string" || f1.type instanceof String);
  1436. const f2RefUndefined = !(typeof f2.type === "string" || f2.type instanceof String);
  1437. if (f1RefUndefined || f2RefUndefined) {
  1438. if (f1RefUndefined && f2RefUndefined) {
  1439. return 0;
  1440. } else if (f1RefUndefined) {
  1441. return 1;
  1442. } else {
  1443. return -1;
  1444. }
  1445. }
  1446. return 0;
  1447. });
  1448. for (const featArray of [this.parsed.feats, this.parsed.specials]) {
  1449. for (const pBFeat of featArray) {
  1450. if (pBFeat.added) continue;
  1451. src_logger.debug("Generating feature for", pBFeat);
  1452. const indexMatch = this.#indexFind(index, [pBFeat.name, pBFeat.originalName]);
  1453. const displayName = pBFeat.extra ? `${pBFeat.name} (${pBFeat.extra})` : pBFeat.name;
  1454. if (!indexMatch) {
  1455. src_logger.debug(`Unable to match feat ${displayName}`, { displayName, name: pBFeat.name, extra: pBFeat.extra, pBFeat, compendiumLabel });
  1456. this.check[pBFeat.originalName] = { name: displayName, type: "feat", details: { displayName, name: pBFeat.name, originalName: pBFeat.originalName, extra: pBFeat.extra, pBFeat, compendiumLabel } };
  1457. continue;
  1458. }
  1459. if (this.check[pBFeat.originalName]) delete this.check[pBFeat.originalName];
  1460. pBFeat.added = true;
  1461. if (this.autoAddedFeatureIds.has(`${indexMatch._id}${indexMatch.type}`)) {
  1462. src_logger.debug("Feat included in class features auto add", { displayName, pBFeat, compendiumLabel });
  1463. continue;
  1464. }
  1465. const doc = await compendium.getDocument(indexMatch._id);
  1466. const item = doc.toObject();
  1467. item._id = foundry.utils.randomID();
  1468. item.name = displayName;
  1469. this.#generateFoundryFeatLocation(item, pBFeat);
  1470. this.result.feats.push(item);
  1471. await this.#addGrantedItems(item);
  1472. }
  1473. }
  1474. }
  1475. async #generateSpecialItems(compendiumLabel) {
  1476. const compendium = await game.packs.get(compendiumLabel);
  1477. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  1478. for (const special of this.parsed.specials) {
  1479. if (special.added) continue;
  1480. src_logger.debug("Generating special for", special);
  1481. const indexMatch = this.#indexFind(index, [special.name, special.originalName]);
  1482. if (!indexMatch) {
  1483. src_logger.debug(`Unable to match special ${special.name}`, { special: special.name, compendiumLabel });
  1484. this.check[special.originalName] = { name: special.name, type: "special", details: { displayName: special.name, name: special.name, originalName: special.originalName, special } };
  1485. continue;
  1486. }
  1487. special.added = true;
  1488. if (this.check[special.originalName]) delete this.check[special.originalName];
  1489. if (this.autoAddedFeatureIds.has(`${indexMatch._id}${indexMatch.type}`)) {
  1490. src_logger.debug("Special included in class features auto add", { special: special.name, compendiumLabel });
  1491. continue;
  1492. }
  1493. const doc = await compendium.getDocument(indexMatch._id);
  1494. const docData = doc.toObject();
  1495. docData._id = foundry.utils.randomID();
  1496. this.result.feats.push(docData);
  1497. await this.#addGrantedItems(docData);
  1498. }
  1499. }
  1500. async #generateEquipmentItems(pack = "pf2e.equipment-srd") {
  1501. const compendium = game.packs.get(pack);
  1502. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  1503. const compendiumBackpack = await compendium.getDocument("3lgwjrFEsQVKzhh7");
  1504. const adventurersPack = this.parsed.equipment.find((e) => e.pbName === "Adventurer's Pack");
  1505. const backpackInstance = adventurersPack ? compendiumBackpack.toObject() : null;
  1506. if (backpackInstance) {
  1507. adventurersPack.added = true;
  1508. backpackInstance._id = foundry.utils.randomID();
  1509. this.result.adventurersPack.item = adventurersPack;
  1510. this.result.equipment.push(backpackInstance);
  1511. for (const content of this.result.adventurersPack.contents) {
  1512. const indexMatch = index.find((i) => i.system.slug === content.slug);
  1513. if (!indexMatch) {
  1514. src_logger.error(`Unable to match adventurers kit item ${content.name}`, content);
  1515. continue;
  1516. }
  1517. const doc = await compendium.getDocument(indexMatch._id);
  1518. const itemData = doc.toObject();
  1519. itemData._id = foundry.utils.randomID();
  1520. itemData.system.quantity = content.qty;
  1521. itemData.system.containerId = backpackInstance?._id;
  1522. this.result.equipment.push(itemData);
  1523. }
  1524. }
  1525. for (const e of this.parsed.equipment) {
  1526. if (e.pbName === "Adventurer's Pack") continue;
  1527. if (e.added) continue;
  1528. if (this.IGNORED_EQUIPMENT.includes(e.pbName)) {
  1529. e.added = true;
  1530. continue;
  1531. }
  1532. src_logger.debug("Generating item for", e);
  1533. const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(e.pbName));
  1534. if (!indexMatch) {
  1535. src_logger.error(`Unable to match ${e.pbName}`, e);
  1536. this.bad.push({ pbName: e.pbName, type: "equipment", details: { e } });
  1537. continue;
  1538. }
  1539. const doc = await compendium.getDocument(indexMatch._id);
  1540. if (doc.type != "kit") {
  1541. const itemData = doc.toObject();
  1542. itemData._id = foundry.utils.randomID();
  1543. itemData.system.quantity = e.qty;
  1544. const type = doc.type === "treasure" ? "treasure" : "equipment";
  1545. this.result[type].push(itemData);
  1546. }
  1547. // eslint-disable-next-line require-atomic-updates
  1548. e.added = true;
  1549. }
  1550. }
  1551. async #generateWeaponItems() {
  1552. const compendium = game.packs.get("pf2e.equipment-srd");
  1553. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  1554. for (const w of this.parsed.weapons) {
  1555. if (this.IGNORED_EQUIPMENT.includes(w.pbName)) {
  1556. w.added = true;
  1557. continue;
  1558. }
  1559. src_logger.debug("Generating weapon for", w);
  1560. const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(w.pbName));
  1561. if (!indexMatch) {
  1562. src_logger.error(`Unable to match weapon item ${w.name}`, w);
  1563. this.bad.push({ pbName: w.pbName, type: "weapon", details: { w } });
  1564. continue;
  1565. }
  1566. const doc = await compendium.getDocument(indexMatch._id);
  1567. const itemData = doc.toObject();
  1568. itemData._id = foundry.utils.randomID();
  1569. itemData.system.quantity = w.qty;
  1570. itemData.system.damage.die = w.die;
  1571. itemData.system.potencyRune.value = w.pot;
  1572. itemData.system.strikingRune.value = w.str;
  1573. if (w.runes[0]) itemData.system.propertyRune1.value = game.pf2e.system.sluggify(w.runes[0], { camel: "dromedary" });
  1574. if (w.runes[1]) itemData.system.propertyRune2.value = game.pf2e.system.sluggify(w.runes[1], { camel: "dromedary" });
  1575. if (w.runes[2]) itemData.system.propertyRune3.value = game.pf2e.system.sluggify(w.runes[2], { camel: "dromedary" });
  1576. if (w.runes[3]) itemData.system.propertyRune4.value = game.pf2e.system.sluggify(w.runes[3], { camel: "dromedary" });
  1577. if (w.mat) {
  1578. const material = w.mat.split(" (")[0];
  1579. itemData.system.preciousMaterial.value = game.pf2e.system.sluggify(material, { camel: "dromedary" });
  1580. itemData.system.preciousMaterialGrade.value = Pathmuncher.getMaterialGrade(w.mat);
  1581. }
  1582. if (w.display) itemData.name = w.display;
  1583. this.result.weapons.push(itemData);
  1584. w.added = true;
  1585. }
  1586. }
  1587. async #generateArmorItems() {
  1588. const compendium = game.packs.get("pf2e.equipment-srd");
  1589. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  1590. for (const a of this.parsed.armor) {
  1591. if (this.IGNORED_EQUIPMENT.includes(a.pbName)) {
  1592. a.added = true;
  1593. continue;
  1594. }
  1595. src_logger.debug("Generating armor for", a);
  1596. const indexMatch = index.find((i) =>
  1597. i.system.slug === game.pf2e.system.sluggify(a.pbName)
  1598. || i.system.slug === game.pf2e.system.sluggify(`${a.pbName} Armor`)
  1599. );
  1600. if (!indexMatch) {
  1601. src_logger.error(`Unable to match armor kit item ${a.name}`, a);
  1602. this.bad.push({ pbName: a.pbName, type: "armor", details: { a } });
  1603. continue;
  1604. }
  1605. const doc = await compendium.getDocument(indexMatch._id);
  1606. const itemData = doc.toObject();
  1607. itemData._id = foundry.utils.randomID();
  1608. itemData.system.equipped.value = a.worn ?? false;
  1609. if (!this.RESTRICTED_EQUIPMENT.some((i) => itemData.name.startsWith(i))) {
  1610. itemData.system.equipped.inSlot = a.worn ?? false;
  1611. itemData.system.quantity = a.qty;
  1612. itemData.system.category = a.prof;
  1613. itemData.system.potencyRune.value = a.pot;
  1614. itemData.system.resiliencyRune.value = a.res;
  1615. const isShield = itemData.system.category === "shield";
  1616. itemData.system.equipped.handsHeld = isShield && a.worn ? 1 : 0;
  1617. itemData.system.equipped.carryType = isShield && a.worn ? "held" : "worn";
  1618. if (a.runes[0]) itemData.system.propertyRune1.value = game.pf2e.system.sluggify(a.runes[0], { camel: "dromedary" });
  1619. if (a.runes[1]) itemData.system.propertyRune2.value = game.pf2e.system.sluggify(a.runes[1], { camel: "dromedary" });
  1620. if (a.runes[2]) itemData.system.propertyRune3.value = game.pf2e.system.sluggify(a.runes[2], { camel: "dromedary" });
  1621. if (a.runes[3]) itemData.system.propertyRune4.value = game.pf2e.system.sluggify(a.runes[3], { camel: "dromedary" });
  1622. if (a.mat) {
  1623. const material = a.mat.split(" (")[0];
  1624. itemData.system.preciousMaterial.value = game.pf2e.system.sluggify(material, { camel: "dromedary" });
  1625. itemData.system.preciousMaterialGrade.value = Pathmuncher.getMaterialGrade(a.mat);
  1626. }
  1627. }
  1628. if (a.display) itemData.name = a.display;
  1629. this.result.armor.push(itemData);
  1630. // eslint-disable-next-line require-atomic-updates
  1631. a.added = true;
  1632. }
  1633. }
  1634. getClassSpellCastingType(dual = false) {
  1635. const classCaster = dual
  1636. ? this.source.spellCasters.find((caster) => caster.name === this.source.dualClass)
  1637. : this.source.spellCasters.find((caster) => caster.name === this.source.class);
  1638. const type = classCaster?.spellcastingType;
  1639. if (type || this.source.spellCasters.length === 0) return type ?? "spontaneous";
  1640. // if no type and multiple spell casters, then return the first spell casting type
  1641. return this.source.spellCasters[0].spellcastingType ?? "spontaneous";
  1642. }
  1643. // aims to determine the class magic tradition for a spellcasting block
  1644. getClassMagicTradition(caster) {
  1645. const classCaster = [this.source.class, this.source.dualClass].includes(caster.name);
  1646. const tradition = classCaster
  1647. ? caster?.magicTradition
  1648. : undefined;
  1649. // if a caster tradition or no spellcasters, return divine
  1650. if (tradition || this.source.spellCasters.length === 0) return tradition ?? "divine";
  1651. // this spell caster type is not a class, determine class tradition based on ability
  1652. const abilityTradition = this.source.spellCasters.find((c) =>
  1653. [this.source.class, this.source.dualClass].includes(c.name)
  1654. && c.ability === caster.ability
  1655. );
  1656. if (abilityTradition) return abilityTradition.magicTradition;
  1657. // final fallback
  1658. // if no type and multiple spell casters, then return the first spell casting type
  1659. return this.source.spellCasters[0].magicTradition && this.source.spellCasters[0].magicTradition !== "focus"
  1660. ? this.source.spellCasters[0].magicTradition
  1661. : "divine";
  1662. }
  1663. async #generateSpellCaster(caster) {
  1664. const isFocus = caster.magicTradition === "focus";
  1665. const magicTradition = this.getClassMagicTradition(caster);
  1666. const spellcastingType = isFocus ? "focus" : caster.spellcastingType;
  1667. const flexible = false; // placeholder
  1668. const name = isFocus ? `${src_utils.capitalize(magicTradition)} ${caster.name}` : caster.name;
  1669. const spellcastingEntity = {
  1670. ability: {
  1671. value: caster.ability,
  1672. },
  1673. proficiency: {
  1674. value: caster.proficiency / 2,
  1675. },
  1676. spelldc: {
  1677. item: 0,
  1678. },
  1679. tradition: {
  1680. value: magicTradition,
  1681. },
  1682. prepared: {
  1683. value: spellcastingType,
  1684. flexible,
  1685. },
  1686. slots: {
  1687. slot0: {
  1688. max: caster.perDay[0],
  1689. prepared: {},
  1690. value: caster.perDay[0],
  1691. },
  1692. slot1: {
  1693. max: caster.perDay[1],
  1694. prepared: {},
  1695. value: caster.perDay[1],
  1696. },
  1697. slot2: {
  1698. max: caster.perDay[2],
  1699. prepared: {},
  1700. value: caster.perDay[2],
  1701. },
  1702. slot3: {
  1703. max: caster.perDay[3],
  1704. prepared: {},
  1705. value: caster.perDay[3],
  1706. },
  1707. slot4: {
  1708. max: caster.perDay[4],
  1709. prepared: {},
  1710. value: caster.perDay[4],
  1711. },
  1712. slot5: {
  1713. max: caster.perDay[5],
  1714. prepared: {},
  1715. value: caster.perDay[5],
  1716. },
  1717. slot6: {
  1718. max: caster.perDay[6],
  1719. prepared: {},
  1720. value: caster.perDay[6],
  1721. },
  1722. slot7: {
  1723. max: caster.perDay[7],
  1724. prepared: {},
  1725. value: caster.perDay[7],
  1726. },
  1727. slot8: {
  1728. max: caster.perDay[8],
  1729. prepared: {},
  1730. value: caster.perDay[8],
  1731. },
  1732. slot9: {
  1733. max: caster.perDay[9],
  1734. prepared: {},
  1735. value: caster.perDay[9],
  1736. },
  1737. slot10: {
  1738. max: caster.perDay[10],
  1739. prepared: {},
  1740. value: caster.perDay[10],
  1741. },
  1742. },
  1743. showUnpreparedSpells: { value: true },
  1744. };
  1745. const data = {
  1746. _id: foundry.utils.randomID(),
  1747. name,
  1748. type: "spellcastingEntry",
  1749. system: spellcastingEntity,
  1750. };
  1751. this.result.casters.push(data);
  1752. return data;
  1753. }
  1754. async #processSpells() {
  1755. const compendium = game.packs.get("pf2e.spells-srd");
  1756. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  1757. const psiCompendium = game.packs.get("pf2e-psychic-amps.psychic-psi-cantrips");
  1758. const psiIndex = psiCompendium ? await psiCompendium.getIndex({ fields: ["name", "type", "system.slug"] }) : undefined;
  1759. for (const caster of this.source.spellCasters) {
  1760. src_logger.debug("Generating caster for", caster);
  1761. if (Number.isInteger(parseInt(caster.focusPoints))) this.result.focusPool += caster.focusPoints;
  1762. const instance = await this.#generateSpellCaster(caster);
  1763. src_logger.debug("Generated caster instance", instance);
  1764. for (const spellSelection of caster.spells) {
  1765. const level = spellSelection.spellLevel;
  1766. for (const [i, spell] of spellSelection.list.entries()) {
  1767. const spellName = spell.split("(")[0].trim();
  1768. src_logger.debug("spell details", { spell, spellName, spellSelection, list: spellSelection.list });
  1769. const psiMatch = psiIndex ? psiIndex.find((i) => i.name === spell) : undefined;
  1770. const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(spellName));
  1771. if (!indexMatch && !psiMatch) {
  1772. src_logger.error(`Unable to match spell ${spell}`, { spell, spellName, spellSelection, caster, instance });
  1773. this.bad.push({ pbName: spell, type: "spell", details: { originalName: spell, name: spellName, spellSelection, caster } });
  1774. continue;
  1775. }
  1776. const doc = psiMatch
  1777. ? await psiCompendium.getDocument(psiMatch._id)
  1778. : await compendium.getDocument(indexMatch._id);
  1779. const itemData = doc.toObject();
  1780. itemData._id = foundry.utils.randomID();
  1781. itemData.system.location.heightenedLevel = level;
  1782. itemData.system.location.value = instance._id;
  1783. this.result.spells.push(itemData);
  1784. instance.system.slots[`slot${level}`].prepared[i] = { id: itemData._id };
  1785. }
  1786. }
  1787. }
  1788. setProperty(this.result.character, "system.resources.focus.max", this.result.focusPool);
  1789. setProperty(this.result.character, "system.resources.focus.value", this.result.focusPool);
  1790. }
  1791. async #generateLores() {
  1792. for (const lore of this.source.lores) {
  1793. const data = {
  1794. name: lore[0],
  1795. type: "lore",
  1796. system: {
  1797. proficient: {
  1798. value: lore[1] / 2,
  1799. },
  1800. featType: "",
  1801. mod: {
  1802. value: 0,
  1803. },
  1804. item: {
  1805. value: 0,
  1806. },
  1807. },
  1808. };
  1809. this.result.lores.push(data);
  1810. }
  1811. }
  1812. async #generateMoney() {
  1813. const compendium = game.packs.get("pf2e.equipment-srd");
  1814. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  1815. const moneyLookup = [
  1816. { slug: "platinum-pieces", type: "pp" },
  1817. { slug: "gold-pieces", type: "gp" },
  1818. { slug: "silver-pieces", type: "sp" },
  1819. { slug: "copper-pieces", type: "cp" },
  1820. ];
  1821. for (const lookup of moneyLookup) {
  1822. const indexMatch = index.find((i) => i.system.slug === lookup.slug);
  1823. if (indexMatch) {
  1824. const doc = await compendium.getDocument(indexMatch._id);
  1825. const itemData = doc.toObject();
  1826. itemData._id = foundry.utils.randomID();
  1827. itemData.system.quantity = this.source.money[lookup.type];
  1828. this.result.money.push(itemData);
  1829. }
  1830. }
  1831. }
  1832. async #processFormulas() {
  1833. const compendium = game.packs.get("pf2e.equipment-srd");
  1834. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  1835. const uuids = [];
  1836. for (const formulaSource of this.source.formula) {
  1837. for (const formulaName of formulaSource.known) {
  1838. const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(formulaName));
  1839. if (!indexMatch) {
  1840. src_logger.error(`Unable to match formula ${formulaName}`, { formulaSource, name: formulaName });
  1841. this.bad.push({ pbName: formulaName, type: "formula", details: { formulaSource, name: formulaName } });
  1842. continue;
  1843. }
  1844. const doc = await compendium.getDocument(indexMatch._id);
  1845. uuids.push({ uuid: doc.uuid });
  1846. }
  1847. }
  1848. setProperty(this.result.character, "system.crafting.formulas", uuids);
  1849. }
  1850. async #processFeats() {
  1851. await this.#generateFeatItems("pf2e.feats-srd");
  1852. await this.#generateFeatItems("pf2e.ancestryfeatures");
  1853. await this.#generateSpecialItems("pf2e.ancestryfeatures");
  1854. await this.#generateSpecialItems("pf2e.classfeatures");
  1855. await this.#generateSpecialItems("pf2e.actionspf2e");
  1856. }
  1857. async #processEquipment() {
  1858. await this.#generateEquipmentItems();
  1859. await this.#generateWeaponItems();
  1860. await this.#generateArmorItems();
  1861. await this.#generateMoney();
  1862. }
  1863. async #generateTempActor(documents = []) {
  1864. const actorData = mergeObject({ type: "character" }, this.result.character);
  1865. actorData.name = "Mr Temp";
  1866. const actor = await Actor.create(actorData);
  1867. const currentState = duplicate(this.result);
  1868. const currentItems = [
  1869. ...(this.options.askForChoices ? this.autoFeats : []),
  1870. ...currentState.feats,
  1871. ...currentState.class,
  1872. ...currentState.background,
  1873. ...currentState.ancestry,
  1874. ...currentState.heritage,
  1875. ...currentState.deity,
  1876. ...currentState.lores,
  1877. ];
  1878. for (const doc of documents) {
  1879. if (!currentItems.some((d) => d._id === doc._id)) {
  1880. currentItems.push(doc);
  1881. }
  1882. }
  1883. try {
  1884. const items = duplicate(currentItems).map((i) => {
  1885. if (i.system.items) i.system.items = [];
  1886. if (i.system.rules) i.system.rules = [];
  1887. return i;
  1888. });
  1889. await actor.createEmbeddedDocuments("Item", items, { keepId: true });
  1890. const ruleIds = currentItems.map((i) => i._id);
  1891. const ruleUpdates = [];
  1892. for (const [key, value] of Object.entries(this.allFeatureRules)) {
  1893. if (ruleIds.includes(key)) {
  1894. ruleUpdates.push({
  1895. _id: key,
  1896. system: {
  1897. // rules: value,
  1898. rules: value.filter((r) => ["GrantItem", "ChoiceSet", "RollOption"].includes(r.key)),
  1899. },
  1900. });
  1901. }
  1902. }
  1903. // console.warn("rule updates", ruleUpdates);
  1904. await actor.updateEmbeddedDocuments("Item", ruleUpdates);
  1905. const itemUpdates = [];
  1906. for (const [key, value] of Object.entries(this.autoAddedFeatureItems)) {
  1907. itemUpdates.push({
  1908. _id: `${key}`,
  1909. system: {
  1910. items: deepClone(value),
  1911. },
  1912. });
  1913. }
  1914. for (const doc of documents) {
  1915. if (getProperty(doc, "system.rules")?.length > 0 && !ruleUpdates.some((r) => r._id === doc._id)) {
  1916. ruleUpdates.push({
  1917. _id: doc._id,
  1918. system: {
  1919. rules: deepClone(doc.system.rules),
  1920. },
  1921. });
  1922. }
  1923. }
  1924. await actor.updateEmbeddedDocuments("Item", itemUpdates);
  1925. src_logger.debug("Final temp actor", actor);
  1926. } catch (err) {
  1927. src_logger.error("Temp actor creation failed", {
  1928. actor,
  1929. documents,
  1930. thisData: deepClone(this.result),
  1931. actorData,
  1932. err,
  1933. currentItems,
  1934. this: this,
  1935. });
  1936. }
  1937. return actor;
  1938. }
  1939. async processCharacter() {
  1940. if (!this.source) return;
  1941. this.#prepare();
  1942. await this.#processCore();
  1943. await this.#processFormulas();
  1944. await this.#processGenericCompendiumLookup("pf2e.deities", this.source.deity, "deity");
  1945. await this.#processGenericCompendiumLookup("pf2e.backgrounds", this.source.background, "background");
  1946. await this.#processGenericCompendiumLookup("pf2e.classes", this.source.class, "class");
  1947. await this.#processGenericCompendiumLookup("pf2e.ancestries", this.source.ancestry, "ancestry");
  1948. await this.#processGenericCompendiumLookup("pf2e.heritages", this.source.heritage, "heritage");
  1949. await this.#detectGrantedFeatures();
  1950. await this.#processFeats();
  1951. await this.#processEquipment();
  1952. await this.#processSpells();
  1953. await this.#generateLores();
  1954. }
  1955. async #removeDocumentsToBeUpdated() {
  1956. const moneyIds = this.actor.items.filter((i) =>
  1957. i.type === "treasure"
  1958. && ["Platinum Pieces", "Gold Pieces", "Silver Pieces", "Copper Pieces"].includes(i.name)
  1959. );
  1960. const classIds = this.actor.items.filter((i) => i.type === "class").map((i) => i._id);
  1961. const deityIds = this.actor.items.filter((i) => i.type === "deity").map((i) => i._id);
  1962. const backgroundIds = this.actor.items.filter((i) => i.type === "background").map((i) => i._id);
  1963. const heritageIds = this.actor.items.filter((i) => i.type === "heritage").map((i) => i._id);
  1964. const ancestryIds = this.actor.items.filter((i) => i.type === "ancestry").map((i) => i._id);
  1965. const treasureIds = this.actor.items.filter((i) => i.type === "treasure" && !moneyIds.includes(i.id)).map((i) => i._id);
  1966. const featIds = this.actor.items.filter((i) => i.type === "feat").map((i) => i._id);
  1967. const actionIds = this.actor.items.filter((i) => i.type === "action").map((i) => i._id);
  1968. const equipmentIds = this.actor.items.filter((i) =>
  1969. i.type === "equipment" || i.type === "backpack" || i.type === "consumable"
  1970. ).map((i) => i._id);
  1971. const weaponIds = this.actor.items.filter((i) => i.type === "weapon").map((i) => i._id);
  1972. const armorIds = this.actor.items.filter((i) => i.type === "armor").map((i) => i._id);
  1973. const loreIds = this.actor.items.filter((i) => i.type === "lore").map((i) => i._id);
  1974. const spellIds = this.actor.items.filter((i) => i.type === "spell" || i.type === "spellcastingEntry").map((i) => i._id);
  1975. const formulaIds = this.actor.system.formulas;
  1976. src_logger.debug("ids", {
  1977. moneyIds,
  1978. deityIds,
  1979. classIds,
  1980. backgroundIds,
  1981. heritageIds,
  1982. ancestryIds,
  1983. treasureIds,
  1984. featIds,
  1985. actionIds,
  1986. equipmentIds,
  1987. weaponIds,
  1988. armorIds,
  1989. loreIds,
  1990. spellIds,
  1991. formulaIds,
  1992. });
  1993. // eslint-disable-next-line complexity
  1994. const keepIds = this.actor.items.filter((i) =>
  1995. (!this.options.addMoney && moneyIds.includes(i._id))
  1996. || (!this.options.addClass && classIds.includes(i._id))
  1997. || (!this.options.addDeity && deityIds.includes(i._id))
  1998. || (!this.options.addBackground && backgroundIds.includes(i._id))
  1999. || (!this.options.addHeritage && heritageIds.includes(i._id))
  2000. || (!this.options.addAncestry && ancestryIds.includes(i._id))
  2001. || (!this.options.addTreasure && treasureIds.includes(i._id))
  2002. || (!this.options.addFeats && (featIds.includes(i._id) || actionIds.includes(i._id)))
  2003. || (!this.options.addEquipment && equipmentIds.includes(i._id))
  2004. || (!this.options.addWeapons && weaponIds.includes(i._id))
  2005. || (!this.options.addArmor && armorIds.includes(i._id))
  2006. || (!this.options.addLores && loreIds.includes(i._id))
  2007. || (!this.options.addSpells && spellIds.includes(i._id))
  2008. ).map((i) => i._id);
  2009. const deleteIds = this.actor.items.filter((i) => !keepIds.includes(i._id)).map((i) => i._id);
  2010. src_logger.debug("ids", {
  2011. deleteIds,
  2012. keepIds,
  2013. });
  2014. await this.actor.deleteEmbeddedDocuments("Item", deleteIds);
  2015. }
  2016. async #createActorEmbeddedDocuments() {
  2017. if (this.options.addDeity) await this.actor.createEmbeddedDocuments("Item", this.result.deity, { keepId: true });
  2018. if (this.options.addAncestry) await this.actor.createEmbeddedDocuments("Item", this.result.ancestry, { keepId: true });
  2019. if (this.options.addHeritage) await this.actor.createEmbeddedDocuments("Item", this.result.heritage, { keepId: true });
  2020. if (this.options.addBackground) await this.actor.createEmbeddedDocuments("Item", this.result.background, { keepId: true });
  2021. if (this.options.addClass) await this.actor.createEmbeddedDocuments("Item", this.result.class, { keepId: true });
  2022. if (this.options.addLores) await this.actor.createEmbeddedDocuments("Item", this.result.lores, { keepId: true });
  2023. // for (const feat of this.result.feats.reverse()) {
  2024. // console.warn(`creating ${feat.name}`, feat);
  2025. // await this.actor.createEmbeddedDocuments("Item", [feat], { keepId: true });
  2026. // }
  2027. if (this.options.addFeats) await this.actor.createEmbeddedDocuments("Item", this.result.feats, { keepId: true });
  2028. if (this.options.addSpells) {
  2029. await this.actor.createEmbeddedDocuments("Item", this.result.casters, { keepId: true });
  2030. await this.actor.createEmbeddedDocuments("Item", this.result.spells, { keepId: true });
  2031. }
  2032. if (this.options.addEquipment) await this.actor.createEmbeddedDocuments("Item", this.result.equipment, { keepId: true });
  2033. if (this.options.addWeapons) await this.actor.createEmbeddedDocuments("Item", this.result.weapons, { keepId: true });
  2034. if (this.options.addArmor) {
  2035. await this.actor.createEmbeddedDocuments("Item", this.result.armor, { keepId: true });
  2036. await this.actor.updateEmbeddedDocuments("Item", this.result.armor, { keepId: true });
  2037. }
  2038. if (this.options.addTreasure) await this.actor.createEmbeddedDocuments("Item", this.result.treasure, { keepId: true });
  2039. if (this.options.addMoney) await this.actor.createEmbeddedDocuments("Item", this.result.money, { keepId: true });
  2040. }
  2041. async #restoreEmbeddedRuleLogic() {
  2042. const importedItems = this.actor.items.map((i) => i._id);
  2043. // Loop back over items and add rule and item progression data back in.
  2044. if (!this.options.askForChoices) {
  2045. src_logger.debug("Restoring logic", { currentActor: duplicate(this.actor) });
  2046. const ruleUpdates = [];
  2047. for (const [key, value] of Object.entries(this.autoAddedFeatureRules)) {
  2048. if (importedItems.includes(key)) {
  2049. ruleUpdates.push({
  2050. _id: `${key}`,
  2051. system: {
  2052. rules: deepClone(value),
  2053. },
  2054. });
  2055. }
  2056. }
  2057. src_logger.debug("Restoring rule logic", ruleUpdates);
  2058. await this.actor.updateEmbeddedDocuments("Item", ruleUpdates);
  2059. const itemUpdates = [];
  2060. for (const [key, value] of Object.entries(this.autoAddedFeatureItems)) {
  2061. if (importedItems.includes(key)) {
  2062. itemUpdates.push({
  2063. _id: `${key}`,
  2064. system: {
  2065. items: deepClone(value),
  2066. },
  2067. });
  2068. }
  2069. }
  2070. src_logger.debug("Restoring granted item logic", itemUpdates);
  2071. await this.actor.updateEmbeddedDocuments("Item", itemUpdates);
  2072. }
  2073. }
  2074. async updateActor() {
  2075. await this.#removeDocumentsToBeUpdated();
  2076. if (!this.options.addName) {
  2077. delete this.result.character.name;
  2078. delete this.result.character.prototypeToken.name;
  2079. }
  2080. if (!this.options.addFormulas) {
  2081. delete this.result.character.system.formulas;
  2082. }
  2083. src_logger.debug("Generated result", this.result);
  2084. await this.actor.update(this.result.character);
  2085. await this.#createActorEmbeddedDocuments();
  2086. await this.#restoreEmbeddedRuleLogic();
  2087. }
  2088. async postImportCheck() {
  2089. const badClass = this.options.addClass
  2090. ? this.bad.filter((b) => b.type === "class").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Class")}: ${b.pbName}</li>`)
  2091. : [];
  2092. const badHeritage = this.options.addHeritage
  2093. ? this.bad.filter((b) => b.type === "heritage").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Heritage")}: ${b.pbName}</li>`)
  2094. : [];
  2095. const badAncestry = this.options.addAncestry
  2096. ? this.bad.filter((b) => b.type === "ancestry").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Ancestry")}: ${b.pbName}</li>`)
  2097. : [];
  2098. const badBackground = this.options.addBackground
  2099. ? this.bad.filter((b) => b.type === "background").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Background")}: ${b.pbName}</li>`)
  2100. : [];
  2101. const badDeity = this.options.addDeity
  2102. ? this.bad.filter((b) => b.type === "deity" && b.pbName !== "Not set").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Deity")}: ${b.pbName}</li>`)
  2103. : [];
  2104. const badFeats = this.options.addFeats
  2105. ? this.bad.filter((b) => b.type === "feat").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Feats")}: ${b.pbName}</li>`)
  2106. : [];
  2107. const badFeats2 = this.options.addFeats
  2108. ? Object.values(this.check).filter((b) =>
  2109. (b.type === "feat" || b.type === "special")
  2110. && this.parsed.feats.concat(this.parsed.specials).some((f) => f.name === b.details.name && !f.added)
  2111. ).map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Feats")}: ${b.details.name}</li>`)
  2112. : [];
  2113. const badEquipment = this.options.addEquipment
  2114. ? this.bad.filter((b) => b.type === "equipment").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Equipment")}: ${b.pbName}</li>`)
  2115. : [];
  2116. const badWeapons = this.options.addWeapons
  2117. ? this.bad.filter((b) => b.type === "weapons").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Weapons")}: ${b.pbName}</li>`)
  2118. : [];
  2119. const badArmor = this.options.addArmor
  2120. ? this.bad.filter((b) => b.type === "armor").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Armor")}: ${b.pbName}</li>`)
  2121. : [];
  2122. const badSpellcasting = this.options.addSpells
  2123. ? this.bad.filter((b) => b.type === "spellcasting").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Spellcasting")}: ${b.pbName}</li>`)
  2124. : [];
  2125. const badSpells = this.options.addSpells
  2126. ? this.bad.filter((b) => b.type === "spells").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Spells")}: ${b.pbName}</li>`)
  2127. : [];
  2128. const badFamiliars = this.options.addFamiliars
  2129. ? this.bad.filter((b) => b.type === "familiars").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Familiars")}: ${b.pbName}</li>`)
  2130. : [];
  2131. const badFormulas = this.options.addFormulas
  2132. ? this.bad.filter((b) => b.type === "formulas").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Formulas")}: ${b.pbName}</li>`)
  2133. : [];
  2134. const totalBad = [
  2135. ...badClass,
  2136. ...badAncestry,
  2137. ...badHeritage,
  2138. ...badBackground,
  2139. ...badDeity,
  2140. ...badFeats,
  2141. ...badFeats2,
  2142. ...badEquipment,
  2143. ...badWeapons,
  2144. ...badArmor,
  2145. ...badSpellcasting,
  2146. ...badSpells,
  2147. ...badFamiliars,
  2148. ...badFormulas,
  2149. ];
  2150. let warning = "";
  2151. if (totalBad.length > 0) {
  2152. warning += `<p>${game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.MissingItemsOpen")}</p><ul>${totalBad.join("\n")}</ul><br>`;
  2153. }
  2154. if (this.result.focusPool > 0) {
  2155. warning += `<strong>${game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.FocusSpells")}</strong><br>`;
  2156. }
  2157. src_logger.debug("Bad thing check", {
  2158. badClass,
  2159. badAncestry,
  2160. badHeritage,
  2161. badBackground,
  2162. badDeity,
  2163. badFeats,
  2164. badFeats2,
  2165. badEquipment,
  2166. badWeapons,
  2167. badArmor,
  2168. badSpellcasting,
  2169. badSpells,
  2170. badFamiliars,
  2171. badFormulas,
  2172. totalBad,
  2173. count: totalBad.length,
  2174. focusPool: this.result.focusPool,
  2175. warning,
  2176. });
  2177. if (totalBad.length > 0 || this.result.focusPool > 0) {
  2178. ui.notifications.warn(game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.CompletedWithNotes"));
  2179. new Dialog({
  2180. title: game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.ImportNotes"),
  2181. content: warning,
  2182. buttons: {
  2183. yes: {
  2184. icon: "<i class='fas fa-check'></i>",
  2185. label: game.i18n.localize("pathmuncher.Labels.Finished"),
  2186. },
  2187. },
  2188. default: "yes",
  2189. }).render(true);
  2190. } else {
  2191. ui.notifications.info(game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.CompletedSuccess"));
  2192. }
  2193. }
  2194. }
  2195. ;// CONCATENATED MODULE: ./src/app/PetShop.js
  2196. /* eslint-disable no-await-in-loop */
  2197. /* eslint-disable no-continue */
  2198. /**
  2199. * The PetShop class looks for familiars in a Pathmunch data set and creates/updates as appropriate.
  2200. */
  2201. class PetShop {
  2202. constructor ({ parent, pathbuilderJson } = {}) {
  2203. this.parent = parent;
  2204. this.pathbuilderJson = pathbuilderJson;
  2205. this.result = {
  2206. pets: [],
  2207. features: {},
  2208. };
  2209. this.bad = {};
  2210. this.folders = {};
  2211. }
  2212. async ensureFolder(type) {
  2213. const folderName = game.i18n.localize(`${constants.MODULE_NAME}.Labels.${type}s`);
  2214. this.folders[type] = await src_utils.getOrCreateFolder(parent.folder, "Actor", folderName);
  2215. }
  2216. async #existingPetCheck(familiarName, type) {
  2217. const existingPet = game.actors.find((a) =>
  2218. a.type === type.toLowerCase()
  2219. && a.name === familiarName
  2220. && a.system.master.id === this.parent._id
  2221. );
  2222. if (existingPet) return existingPet.toObject();
  2223. const actorData = {
  2224. type: type.toLowerCase(),
  2225. name: familiarName,
  2226. system: {
  2227. master: {
  2228. id: this.parent._id,
  2229. ability: this.parent.system.details.keyability.value,
  2230. },
  2231. },
  2232. prototypeToken: {
  2233. name: familiarName,
  2234. },
  2235. folder: this.folders[type].id,
  2236. };
  2237. const actor = await Actor.create(actorData);
  2238. return actor.toObject();
  2239. }
  2240. #buildCore(petData) {
  2241. setProperty(petData, "system.attributes.value", this.parent.system.details.level.value * 5);
  2242. return petData;
  2243. }
  2244. async #generatePetFeatures(pet, json) {
  2245. const compendium = game.packs.get("pf2e.familiar-abilities");
  2246. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  2247. this.result.features[pet._id] = [];
  2248. this.bad[pet._id] = [];
  2249. for (const featureName of json.abilities) {
  2250. const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(featureName));
  2251. if (!indexMatch) {
  2252. src_logger.warn(`Unable to match pet feature ${featureName}`, { pet, json, name: featureName });
  2253. this.bad[pet._id].push({ pbName: featureName, type: "feature", details: { pet, json, name: featureName } });
  2254. continue;
  2255. }
  2256. const doc = (await compendium.getDocument(indexMatch._id)).toObject();
  2257. doc._id = foundry.utils.randomID();
  2258. this.result.features[pet._id].push(doc);
  2259. }
  2260. }
  2261. async buildPet(json) {
  2262. const name = json.name === json.type || !json.name.includes("(")
  2263. ? `${this.parent.name}'s ${json.type}`
  2264. : json.name.split("(")[1].split(")")[0];
  2265. const petData = await this.#existingPetCheck(name, json.type);
  2266. const pet = this.#buildCore(petData);
  2267. await this.#generatePetFeatures(pet, json);
  2268. this.result.pets.push(pet);
  2269. }
  2270. async updatePets() {
  2271. for (const petData of this.result.pets) {
  2272. const actor = game.actors.get(petData._id);
  2273. await actor.deleteEmbeddedDocuments("Item", [], { deleteAll: true });
  2274. await actor.update(petData);
  2275. await actor.createEmbeddedDocuments("Item", this.result.features[petData._id], { keepId: true });
  2276. }
  2277. }
  2278. async processPets() {
  2279. for (const petJson of this.pathbuilderJson.pets) {
  2280. // only support familiars at this time
  2281. if (petJson.type !== "Familiar") {
  2282. src_logger.warn(`Pets with type ${petJson.type} are not supported at this time`);
  2283. continue;
  2284. }
  2285. await this.ensureFolder(petJson.type);
  2286. await this.buildPet(petJson);
  2287. }
  2288. await this.updatePets();
  2289. src_logger.debug("Pets", {
  2290. results: this.results,
  2291. bad: this.bad,
  2292. });
  2293. }
  2294. async addPetEffects() {
  2295. const features = [];
  2296. for (const petData of this.result.pets) {
  2297. for (const feature of this.result.features[petData._id].filter((f) => f.system.rules?.some((r) => r.key === "ActiveEffectLike"))) {
  2298. if (!this.parent.items.some((i) => i.type === "effect" && i.system.slug === feature.system.slug)) {
  2299. features.push(feature);
  2300. }
  2301. }
  2302. }
  2303. await this.parent.createEmbeddedDocuments("Item", features);
  2304. }
  2305. }
  2306. ;// CONCATENATED MODULE: ./src/app/PathmuncherImporter.js
  2307. class PathmuncherImporter extends FormApplication {
  2308. constructor(options, actor) {
  2309. super(options);
  2310. this.actor = game.actors.get(actor.id ? actor.id : actor._id);
  2311. this.backup = duplicate(this.actor);
  2312. this.mode = "number";
  2313. }
  2314. static get defaultOptions() {
  2315. const options = super.defaultOptions;
  2316. options.title = game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.PathmuncherImporter.Title`);
  2317. options.template = `${constants.PATH}/templates/pathmuncher.hbs`;
  2318. options.classes = ["pathmuncher"];
  2319. options.id = "pathmuncher";
  2320. options.width = 400;
  2321. options.closeOnSubmit = false;
  2322. options.tabs = [{ navSelector: ".tabs", contentSelector: "form", initial: "number" }];
  2323. return options;
  2324. }
  2325. /** @override */
  2326. async getData() {
  2327. const flags = src_utils.getFlags(this.actor);
  2328. return {
  2329. flags,
  2330. id: flags?.pathbuilderId ?? "",
  2331. actor: this.actor,
  2332. };
  2333. }
  2334. /** @override */
  2335. activateListeners(html) {
  2336. super.activateListeners(html);
  2337. $("#pathmuncher").css("height", "auto");
  2338. $(html)
  2339. .find('.item')
  2340. .on("click", (event) => {
  2341. if (!event.target?.dataset?.tab) return;
  2342. this.mode = event.target.dataset.tab;
  2343. });
  2344. }
  2345. static _updateProgress(total, count, type) {
  2346. const localizedType = `pathmuncher.Label.${type}`;
  2347. $(".import-progress-bar")
  2348. .width(`${Math.trunc((count / total) * 100)}%`)
  2349. .html(
  2350. `<span>${game.i18n.localize("pathmuncher.Label.Working")} (${game.i18n.localize(localizedType)})...</span>`
  2351. );
  2352. }
  2353. async _updateObject(event, formData) {
  2354. const pathbuilderId = formData.textBoxBuildID;
  2355. const options = {
  2356. pathbuilderId,
  2357. addMoney: formData.checkBoxMoney,
  2358. addFeats: formData.checkBoxFeats,
  2359. addSpells: formData.checkBoxSpells,
  2360. addEquipment: formData.checkBoxEquipment,
  2361. addTreasure: formData.checkBoxTreasure,
  2362. addLores: formData.checkBoxLores,
  2363. addWeapons: formData.checkBoxWeapons,
  2364. addArmor: formData.checkBoxArmor,
  2365. addDeity: formData.checkBoxDeity,
  2366. addName: formData.checkBoxName,
  2367. addClass: formData.checkBoxClass,
  2368. addBackground: formData.checkBoxBackground,
  2369. addHeritage: formData.checkBoxHeritage,
  2370. addAncestry: formData.checkBoxAncestry,
  2371. addFamiliars: formData.checkBoxFamiliars,
  2372. addFormulas: formData.checkBoxFormulas,
  2373. askForChoices: formData.checkBoxAskForChoices,
  2374. };
  2375. src_logger.debug("Pathmuncher options", options);
  2376. await src_utils.setFlags(this.actor, options);
  2377. const pathmuncher = new Pathmuncher(this.actor, options);
  2378. if (this.mode === "number") {
  2379. await pathmuncher.fetchPathbuilder(pathbuilderId);
  2380. } else if (this.mode === "json") {
  2381. try {
  2382. const jsonData = JSON.parse(formData.textBoxBuildJSON);
  2383. pathmuncher.source = jsonData.build;
  2384. } catch (err) {
  2385. ui.notifications.error("Unable to parse JSON data");
  2386. return;
  2387. }
  2388. }
  2389. src_logger.debug("Pathmuncher Source", pathmuncher.source);
  2390. await pathmuncher.processCharacter();
  2391. src_logger.debug("Post processed character", pathmuncher);
  2392. await pathmuncher.updateActor();
  2393. src_logger.debug("Final import details", {
  2394. actor: this.actor,
  2395. pathmuncher,
  2396. options,
  2397. pathbuilderSource: pathmuncher.source,
  2398. pathbuilderId,
  2399. });
  2400. if (options.addFamiliars) {
  2401. const petShop = new PetShop({ parent: this.actor, pathbuilderJson: pathmuncher.source });
  2402. await petShop.processPets();
  2403. await petShop.addPetEffects();
  2404. }
  2405. this.close();
  2406. await pathmuncher.postImportCheck();
  2407. }
  2408. }
  2409. ;// CONCATENATED MODULE: ./src/hooks/api.js
  2410. function registerAPI() {
  2411. game.modules.get(constants.MODULE_NAME).api = {
  2412. Pathmuncher: Pathmuncher,
  2413. PathmuncherImporter: PathmuncherImporter,
  2414. data: {
  2415. generateFeatMap: FEAT_RENAME_MAP,
  2416. equipment: EQUIPMENT_RENAME_MAP,
  2417. restrictedEquipment: RESTRICTED_EQUIPMENT,
  2418. feats: FEAT_RENAME_MAP(),
  2419. },
  2420. utils: src_utils,
  2421. CONSTANTS: constants,
  2422. };
  2423. }
  2424. ;// CONCATENATED MODULE: ./src/hooks/settings.js
  2425. async function resetSettings() {
  2426. for (const [name, data] of Object.entries(constants.GET_DEFAULT_SETTINGS())) {
  2427. // eslint-disable-next-line no-await-in-loop
  2428. await game.settings.set(constants.MODULE_NAME, name, data.default);
  2429. }
  2430. window.location.reload();
  2431. }
  2432. class ResetSettingsDialog extends FormApplication {
  2433. constructor(...args) {
  2434. super(...args);
  2435. // eslint-disable-next-line no-constructor-return
  2436. return new Dialog({
  2437. title: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Title`),
  2438. content: `<p class="${constants.FLAG_NAME}-dialog-important">${game.i18n.localize(
  2439. `${constants.FLAG_NAME}.Dialogs.ResetSettings.Content`
  2440. )}</p>`,
  2441. buttons: {
  2442. confirm: {
  2443. icon: '<i class="fas fa-check"></i>',
  2444. label: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Confirm`),
  2445. callback: () => {
  2446. resetSettings();
  2447. },
  2448. },
  2449. cancel: {
  2450. icon: '<i class="fas fa-times"></i>',
  2451. label: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Cancel`),
  2452. },
  2453. },
  2454. default: "cancel",
  2455. });
  2456. }
  2457. }
  2458. function registerSettings() {
  2459. game.settings.registerMenu(constants.MODULE_NAME, "resetToDefaults", {
  2460. name: `${constants.FLAG_NAME}.Settings.Reset.Title`,
  2461. label: `${constants.FLAG_NAME}.Settings.Reset.Label`,
  2462. hint: `${constants.FLAG_NAME}.Settings.Reset.Hint`,
  2463. icon: "fas fa-refresh",
  2464. type: ResetSettingsDialog,
  2465. restricted: true,
  2466. });
  2467. for (const [name, data] of Object.entries(constants.GET_DEFAULT_SETTINGS())) {
  2468. game.settings.register(constants.MODULE_NAME, name, data);
  2469. }
  2470. }
  2471. ;// CONCATENATED MODULE: ./src/hooks/sheets.js
  2472. function registerSheetButton() {
  2473. const trustedUsersOnly = src_utils.setting(constants.SETTINGS.RESTRICT_TO_TRUSTED);
  2474. if (trustedUsersOnly && !game.user.isTrusted) return;
  2475. /**
  2476. * Character sheets
  2477. */
  2478. const pcSheetNames = Object.values(CONFIG.Actor.sheetClasses.character)
  2479. .map((sheetClass) => sheetClass.cls)
  2480. .map((sheet) => sheet.name);
  2481. pcSheetNames.forEach((sheetName) => {
  2482. Hooks.on("render" + sheetName, (app, html, data) => {
  2483. // only for GMs or the owner of this character
  2484. if (!data.owner || !data.actor) return;
  2485. const button = $(`<a class="pathmuncher-open" title="${constants.MODULE_FULL_NAME}"><i class="fas fa-hat-wizard"></i> Pathmuncher</a>`);
  2486. button.click(() => {
  2487. const muncher = new PathmuncherImporter(PathmuncherImporter.defaultOptions, data.actor);
  2488. muncher.render(true);
  2489. });
  2490. html.closest('.app').find('.pathmuncher-open').remove();
  2491. let titleElement = html.closest('.app').find('.window-title');
  2492. if (!app._minimized) button.insertAfter(titleElement);
  2493. });
  2494. });
  2495. }
  2496. ;// CONCATENATED MODULE: ./src/module.js
  2497. Hooks.once("init", () => {
  2498. registerSettings();
  2499. });
  2500. Hooks.once("ready", () => {
  2501. registerSheetButton();
  2502. registerAPI();
  2503. });
  2504. /******/ })()
  2505. ;
  2506. //# sourceMappingURL=main.js.map