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.

4901 lines
182 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
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. USE_CUSTOM_COMPENDIUM_MAPPINGS: "use-custom-compendium-mappings",
  16. CUSTOM_COMPENDIUM_MAPPINGS: "custom-compendium-mappings",
  17. USE_IMMEDIATE_DEEP_DIVE: "use-immediate-deep-dive",
  18. },
  19. FEAT_PRIORITY: [
  20. "Heritage",
  21. "Heritage Feat",
  22. "Ancestry",
  23. "Ancestry Feat",
  24. "Background",
  25. "Background Feat",
  26. "Class Feat",
  27. "Skill Feat",
  28. "General Feat",
  29. "Awarded Feat",
  30. ],
  31. ACTOR_FLAGS: {
  32. pathbuilderId: undefined,
  33. addFeats: true,
  34. addEquipment: true,
  35. addBackground: true,
  36. addHeritage: true,
  37. addAncestry: true,
  38. addSpells: true,
  39. adjustBlendedSlots: true,
  40. addMoney: true,
  41. addTreasure: true,
  42. addLores: true,
  43. addWeapons: true,
  44. addArmor: true,
  45. addDeity: true,
  46. addName: true,
  47. addClass: true,
  48. addFamiliars: true,
  49. addFormulas: true,
  50. },
  51. CORE_COMPENDIUM_MAPPINGS: {
  52. feats: [
  53. "pf2e.feats-srd",
  54. "pf2e-playtest-data.war-of-immortals-playtest-class-feats",
  55. "pf2e-legacy-content.feats-legacy",
  56. ],
  57. ancestryFeatures: ["pf2e.ancestryfeatures", "pf2e-legacy-content.ancestry-features-legacy"],
  58. classFeatures: [
  59. "pf2e.classfeatures",
  60. "pf2e-playtest-data.war-of-immortals-playtest-class-features",
  61. "pf2e-legacy-content.class-features-legacy",
  62. ],
  63. actions: [
  64. "pf2e.actionspf2e",
  65. "pf2e-playtest-data.war-of-immortals-playtest-actions",
  66. "pf2e-legacy-content.actions-legacy",
  67. ],
  68. spells: [
  69. "pf2e-psychic-amps.psychic-psi-cantrips",
  70. "pf2e.spells-srd",
  71. "pf2e-playtest-data.war-of-immortals-playtest-spells",
  72. "pf2e-legacy-content.spells-legacy",
  73. ],
  74. classes: [
  75. "pf2e.classes",
  76. "pf2e-playtest-data.war-of-immortals-playtest-classes",
  77. "pf2e-legacy-content.classes-legacy",
  78. ],
  79. ancestries: ["pf2e.ancestries", "pf2e-legacy-content.ancestries-legacy"],
  80. heritages: ["pf2e.heritages", "pf2e-legacy-content.heritages-legacy"],
  81. equipment: ["pf2e.equipment-srd", "pf2e-legacy-content.equipment-legacy"],
  82. formulas: ["pf2e.equipment-srd", "pf2e-legacy-content.formulas-legacy"],
  83. deities: ["pf2e.deities", "pf2e-legacy-content.deities-legacy"],
  84. backgrounds: ["pf2e.backgrounds", "pf2e-legacy-content.backgrounds-legacy"],
  85. },
  86. GET_DEFAULT_SETTINGS() {
  87. return foundry.utils.deepClone(CONSTANTS.DEFAULT_SETTINGS);
  88. },
  89. };
  90. CONSTANTS.DEFAULT_SETTINGS = {
  91. // Enable options
  92. [CONSTANTS.SETTINGS.RESTRICT_TO_TRUSTED]: {
  93. name: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Name`,
  94. hint: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Hint`,
  95. scope: "world",
  96. config: true,
  97. type: Boolean,
  98. default: false,
  99. onChange: debouncedReload,
  100. },
  101. [CONSTANTS.SETTINGS.USE_CUSTOM_COMPENDIUM_MAPPINGS]: {
  102. name: `${CONSTANTS.FLAG_NAME}.Settings.UseCustomCompendiumMappings.Name`,
  103. scope: "world",
  104. config: false,
  105. type: Boolean,
  106. default: false,
  107. },
  108. [CONSTANTS.SETTINGS.USE_IMMEDIATE_DEEP_DIVE]: {
  109. name: `${CONSTANTS.FLAG_NAME}.Settings.UseImmediateDeepDive.Name`,
  110. scope: "world",
  111. config: false,
  112. type: Boolean,
  113. default: true,
  114. },
  115. [CONSTANTS.SETTINGS.CUSTOM_COMPENDIUM_MAPPINGS]: {
  116. scope: "world",
  117. config: false,
  118. type: Object,
  119. default: {
  120. feats: [
  121. "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-feats",
  122. "battlezoo-ancestries-year-of-monsters-pf2e.yom-features",
  123. "battlezoo-ancestries-year-of-monsters-pf2e.yom-feats",
  124. "clerics.clerics-feats",
  125. "clerics.clerics-features",
  126. "pf2e.feats-srd",
  127. "pf2e-playtest-data.war-of-immortals-playtest-class-feats",
  128. ],
  129. ancestryFeatures: [
  130. "battlezoo-ancestries-year-of-monsters-pf2e.yom-features",
  131. "pf2e.ancestryfeatures",
  132. ],
  133. classFeatures: [
  134. "battlezoo-ancestries-year-of-monsters-pf2e.yom-features",
  135. "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-feats",
  136. "battlezoo-ancestries-year-of-monsters-pf2e.yom-feats",
  137. "clerics.clerics-doctrines",
  138. "clerics.clerics-feats",
  139. "clerics.clerics-features",
  140. "pf2e.classfeatures",
  141. "pf2e-playtest-data.war-of-immortals-playtest-class-features",
  142. ],
  143. actions: ["pf2e.actionspf2e", "pf2e-playtest-data.war-of-immortals-playtest-actions"],
  144. spells: ["pf2e-psychic-amps.psychic-psi-cantrips", "pf2e.spells-srd", "pf2e-playtest-data.war-of-immortals-playtest-spells"],
  145. classes: ["clerics.clerics-features", "pf2e.classes", "pf2e-playtest-data.war-of-immortals-playtest-classes"],
  146. ancestries: [
  147. "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-ancestry",
  148. "battlezoo-ancestries-year-of-monsters-pf2e.yom-ancestries",
  149. "pf2e.ancestries",
  150. ],
  151. heritages: [
  152. "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-heritages",
  153. "battlezoo-ancestries-year-of-monsters-pf2e.yom-heritages",
  154. "pf2e.heritages",
  155. ],
  156. equipment: [
  157. "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-equipment",
  158. "battlezoo-ancestries-year-of-monsters-pf2e.yom-equipment",
  159. "pf2e.equipment-srd"
  160. ],
  161. formulas: ["pf2e.equipment-srd"],
  162. deities: ["clerics.clerics-deities", "pf2e.deities"],
  163. backgrounds: ["pf2e.backgrounds"],
  164. },
  165. },
  166. [CONSTANTS.SETTINGS.ADD_VISION_FEATS]: {
  167. name: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Name`,
  168. hint: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Hint`,
  169. scope: "player",
  170. config: true,
  171. type: Boolean,
  172. default: false,
  173. },
  174. // debug
  175. [CONSTANTS.SETTINGS.LOG_LEVEL]: {
  176. name: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Name`,
  177. hint: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Hint`,
  178. scope: "world",
  179. config: true,
  180. type: String,
  181. choices: {
  182. DEBUG: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.debug`,
  183. INFO: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.info`,
  184. WARN: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.warn`,
  185. ERR: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.error`,
  186. OFF: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.off`,
  187. },
  188. default: "WARN",
  189. }
  190. };
  191. CONSTANTS.PATH = `modules/${CONSTANTS.MODULE_NAME}`;
  192. /* harmony default export */ const constants = (CONSTANTS);
  193. ;// CONCATENATED MODULE: ./src/utils.js
  194. const utils = {
  195. isObject: (obj) => {
  196. return typeof obj === 'object' && !Array.isArray(obj) && obj !== null;
  197. },
  198. isString: (str) => {
  199. return typeof str === 'string' || str instanceof String;
  200. },
  201. wait: async (ms) => {
  202. return new Promise((resolve) => {
  203. setTimeout(resolve, ms);
  204. });
  205. },
  206. capitalize: (s) => {
  207. if (typeof s !== "string") return "";
  208. return s.charAt(0).toUpperCase() + s.slice(1);
  209. },
  210. setting: (key) => {
  211. return game.settings.get(constants.MODULE_NAME, constants.SETTINGS[key]);
  212. },
  213. updateSetting: async (key, value) => {
  214. return game.settings.set(constants.MODULE_NAME, constants.SETTINGS[key], value);
  215. },
  216. getFlags: (actor) => {
  217. const flags = actor.flags[constants.FLAG_NAME]
  218. ? actor.flags[constants.FLAG_NAME]
  219. : constants.ACTOR_FLAGS;
  220. return flags;
  221. },
  222. setFlags: async (actor, flags) => {
  223. let updateData = {};
  224. setProperty(updateData, `flags.${constants.FLAG_NAME}`, flags);
  225. await actor.update(updateData);
  226. return actor;
  227. },
  228. resetFlags: async (actor) => {
  229. return utils.setFlags(actor, null);
  230. },
  231. getOrCreateFolder: async (root, entityType, folderName, folderColor = "") => {
  232. let folder = game.folders.contents.find((f) =>
  233. f.type === entityType && f.name === folderName
  234. // if a root folder we want to match the root id for the parent folder
  235. && (root ? root.id : null) === (f.folder?.id ?? null)
  236. );
  237. // console.warn(`Looking for ${root} ${entityType} ${folderName}`);
  238. // console.warn(folder);
  239. if (folder) return folder;
  240. folder = await Folder.create(
  241. {
  242. name: folderName,
  243. type: entityType,
  244. color: folderColor,
  245. parent: (root) ? root.id : null,
  246. },
  247. { displaySheet: false }
  248. );
  249. return folder;
  250. },
  251. // eslint-disable-next-line no-unused-vars
  252. getFolder: async (kind, subFolder = "", baseFolderName = "Pathmuncher", baseColor = "#6f0006", subColor = "#98020a", typeFolder = true) => {
  253. let entityTypes = new Map();
  254. entityTypes.set("pets", "Pets");
  255. const folderName = game.i18n.localize(`${constants.MODULE_NAME}.labels.${kind}`);
  256. const entityType = entityTypes.get(kind);
  257. const baseFolder = await utils.getOrCreateFolder(null, entityType, baseFolderName, baseColor);
  258. const entityFolder = typeFolder ? await utils.getOrCreateFolder(baseFolder, entityType, folderName, subColor) : baseFolder;
  259. if (subFolder !== "") {
  260. const subFolderName = subFolder.charAt(0).toUpperCase() + subFolder.slice(1);
  261. const typeFolder = await utils.getOrCreateFolder(entityFolder, entityType, subFolderName, subColor);
  262. return typeFolder;
  263. } else {
  264. return entityFolder;
  265. }
  266. },
  267. allowDualClasses: () => {
  268. return (isNewerVersion("5.9.0", game.version) && game.settings.get("pf2e", "dualClassVariant"));
  269. // || (!isNewerVersion("5.9.0", game.version) && when remaster supports dualclass then add here
  270. },
  271. allowAncestryParagon: () => {
  272. return (isNewerVersion("5.9.0", game.version) && game.settings.get("pf2e", "ancestryParagonVariant"));
  273. }
  274. };
  275. /* harmony default export */ const src_utils = (utils);
  276. ;// CONCATENATED MODULE: ./src/logger.js
  277. const logger = {
  278. _showMessage: (logLevel, data) => {
  279. if (!logLevel || !data || typeof logLevel !== "string") {
  280. return false;
  281. }
  282. const setting = src_utils.setting("LOG_LEVEL");
  283. const logLevels = ["DEBUG", "INFO", "WARN", "ERR", "OFF"];
  284. const logLevelIndex = logLevels.indexOf(logLevel.toUpperCase());
  285. if (setting == "OFF" || logLevelIndex === -1 || logLevelIndex < logLevels.indexOf(setting)) {
  286. return false;
  287. }
  288. return true;
  289. },
  290. log: (logLevel, ...data) => {
  291. if (!logger._showMessage(logLevel, data)) {
  292. return;
  293. }
  294. logLevel = logLevel.toUpperCase();
  295. let msg = "No logging message provided. Please see the payload for more information.";
  296. let payload = data.slice();
  297. if (data[0] && typeof (data[0] == "string")) {
  298. msg = data[0];
  299. if (data.length > 1) {
  300. payload = data.slice(1);
  301. } else {
  302. payload = null;
  303. }
  304. }
  305. msg = `${constants.MODULE_NAME} | ${logLevel} > ${msg}`;
  306. switch (logLevel) {
  307. case "DEBUG":
  308. if (payload) {
  309. console.debug(msg, ...payload); // eslint-disable-line no-console
  310. } else {
  311. console.debug(msg); // eslint-disable-line no-console
  312. }
  313. break;
  314. case "INFO":
  315. if (payload) {
  316. console.info(msg, ...payload); // eslint-disable-line no-console
  317. } else {
  318. console.info(msg); // eslint-disable-line no-console
  319. }
  320. break;
  321. case "WARN":
  322. if (payload) {
  323. console.warn(msg, ...payload); // eslint-disable-line no-console
  324. } else {
  325. console.warn(msg); // eslint-disable-line no-console
  326. }
  327. break;
  328. case "ERR":
  329. if (payload) {
  330. console.error(msg, ...payload); // eslint-disable-line no-console
  331. } else {
  332. console.error(msg); // eslint-disable-line no-console
  333. }
  334. break;
  335. default:
  336. break;
  337. }
  338. },
  339. debug: (...data) => {
  340. logger.log("DEBUG", ...data);
  341. },
  342. info: (...data) => {
  343. logger.log("INFO", ...data);
  344. },
  345. warn: (...data) => {
  346. logger.log("WARN", ...data);
  347. },
  348. error: (...data) => {
  349. logger.log("ERR", ...data);
  350. },
  351. };
  352. /* harmony default export */ const src_logger = (logger);
  353. ;// CONCATENATED MODULE: ./src/data/equipment.js
  354. const SWAPS = [
  355. /^(Greater) (.*)/,
  356. /^(Lesser) (.*)/,
  357. /^(Major) (.*)/,
  358. /^(Moderate) (.*)/,
  359. /^(Standard) (.*)/,
  360. ];
  361. const POSTFIX_PB_REMOVALS = [
  362. /(.*) (- Melee)$/,
  363. /(.*) (- Ranged)$/,
  364. /(Charm of Resistance .*) - (.*)/,
  365. ];
  366. const PARENTHESIS = [
  367. /^(.*) \((.*)\)$/,
  368. ];
  369. const SPLITS = [
  370. // /^(.*) - (.*)/,
  371. ];
  372. const SPLITS_INVERT = [
  373. /^(.*): (.*)/,
  374. ];
  375. const REPLACES = [
  376. { pbName: "Ring of Energy Resistance", foundryName: "Charm of Resistance" },
  377. { pbName: "Feather Token", foundryName: "Marvelous Miniatures" },
  378. { pbName: "Goggles of Night", foundryName: "Obsidian Goggles" },
  379. ];
  380. // this equipment is named differently in foundry vs pathbuilder
  381. const EQUIPMENT_RENAME_STATIC_MAP = [
  382. { pbName: "Inventor Power Suit", foundryName: "Power Suit" },
  383. { pbName: "Inventor Power Suit (Heavy)", foundryName: "Power Suit" },
  384. { pbName: "Inventor Subterfuge Suit", foundryName: "Subterfuge Suit" },
  385. { pbName: "Chain", foundryName: "Chain (10 feet)" },
  386. { pbName: "Oil", foundryName: "Oil (1 pint)" },
  387. { pbName: "Bracelets of Dashing", foundryName: "Bracelet of Dashing" },
  388. { pbName: "Fingerprinting Kit", foundryName: "Fingerprint Kit" },
  389. { pbName: "Ladder", foundryName: "Ladder (10-foot)" },
  390. { pbName: "Mezmerizing Opal", foundryName: "Mesmerizing Opal" },
  391. // { pbName: "Explorer's Clothing", foundryName: "Clothing (Explorer's)" },
  392. { pbName: "Flaming Star (Greater)", foundryName: "Greater Flaming Star" },
  393. { pbName: "Potion of Lesser Darkvision", foundryName: "Darkvision Elixir (Lesser)" },
  394. { pbName: "Potion of Greater Darkvision", foundryName: "Darkvision Elixir (Greater)" },
  395. { pbName: "Potion of Moderate Darkvision", foundryName: "Darkvision Elixir (Moderate)" },
  396. { pbName: "Bottled Sunlight", foundryName: "Formulated Sunlight" },
  397. { pbName: "Magazine (Repeating Hand Crossbow)", foundryName: "Magazine with 5 Bolts" },
  398. { pbName: "Astrolabe (Standard)", foundryName: "Standard Astrolabe" },
  399. { pbName: "Skinitch Salve", foundryName: "Skinstitch Salve" },
  400. { pbName: "Flawless Scale", foundryName: "Abadar's Flawless Scale" },
  401. { pbName: "Construct Key", foundryName: "Cordelia's Construct Key" },
  402. { pbName: "Construct Key (Greater)", foundryName: "Cordelia's Greater Construct Key" },
  403. { pbName: "Lesser Swapping Stone", foundryName: "Lesser Bonmuan Swapping Stone" },
  404. { pbName: "Major Swapping Stone", foundryName: "Major Bonmuan Swapping Stone" },
  405. { pbName: "Moderate Swapping Stone", foundryName: "Moderate Bonmuan Swapping Stone" },
  406. { pbName: "Greater Swapping Stone", foundryName: "Greater Bonmuan Swapping Stone" },
  407. { pbName: "Heartstone", foundryName: "Skarja's Heartstone" },
  408. { pbName: "Bullets (10 rounds)", foundryName: "Sling Bullets" },
  409. { pbName: "Hide", foundryName: "Hide Armor" },
  410. { pbName: "Soverign Glue", foundryName: "Sovereign Glue" },
  411. { pbName: "Axe Musket - Melee", foundryName: "Axe Musket" },
  412. { pbName: "Axe Musket - Ranged", foundryName: "Axe Musket" },
  413. { pbName: "Extendible Pincer", foundryName: "Extendable Pincer" },
  414. { pbName: "Clothing (Explorer's)", foundryName: "Explorer's Clothing" },
  415. { pbName: "Street Preacher [Placeholder]", foundryName: "Street Preacher" },
  416. { pbName: "Repair Kit", foundryName: "Repair Toolkit" },
  417. { pbName: "Repair Kit (Superb)", foundryName: "Repair Toolkit (Superb)" },
  418. { pbName: "Alchemist's Tools", foundryName: "Alchemist's Toolkit" },
  419. { pbName: "Healer's Tools", foundryName: "Healer's Toolkit" },
  420. { pbName: "Healer's Tools (Expanded)", foundryName: "Healer's Toolkit (Expanded)" },
  421. { pbName: "Thieves' Tools", foundryName: "Thieves' Toolkit" },
  422. { pbName: "Thieves' Tools (Infiltrator)", foundryName: "Thieves' Toolkit (Infiltrator)" },
  423. { pbName: "Thieves' Tools (Infiltrator Picks)", foundryName: "Thieves' Toolkit (Infiltrator Picks)" },
  424. { pbName: "Artisan's Tools", foundryName: "Artisan's Toolkit" },
  425. { pbName: "Artisan's Tools (Sterling)", foundryName: "Artisan's Toolkit (Sterling)" },
  426. { pbName: "Aeon Stone (Dull Grey)", foundryName: "Aeon Stone (Consumed)" },
  427. { pbName: "Aeon Stone (Clear Spindle)", foundryName: "Aeon Stone (Nourishing)" },
  428. { pbName: "Aeon Stone (Tourmaline Sphere)", foundryName: "Aeon Stone (Delaying)" },
  429. { pbName: "Aeon Stone (Orange Prism)", foundryName: "Aeon Stone (Amplifying)" },
  430. { pbName: "Bag of Holding", foundryName: "Spacious Pouch" },
  431. { pbName: "Barkskin Potion", foundryName: "Oak Potion" },
  432. { pbName: "Boots of Speed", foundryName: "Propulsive Boots" },
  433. { pbName: "Bracers of Armor", foundryName: "Bands of Force" },
  434. { pbName: "Broom of Flying", foundryName: "Flying Broomstick" },
  435. { pbName: "Dagger of Venom", foundryName: "Serpent Dagger" },
  436. // these are actually matched to energy type witch Pathbuilder does not support
  437. { pbName: "Dragon's Breath Potion (Young)", foundryName: "Energy Breath Potion (Lesser)" },
  438. { pbName: "Dragon's Breath Potion (Adult)", foundryName: "Energy Breath Potion (Moderate)" },
  439. { pbName: "Dragon's Breath Potion (Wyrm)", foundryName: "Energy Breath Potion (Greater)" },
  440. { pbName: "Druid's Vestments", foundryName: "Living Mantle" },
  441. { pbName: "Everburning Torch", foundryName: "Everlight Crystal" },
  442. { pbName: "Eyes of the Eagle", foundryName: "Eyes of the Cat" },
  443. { pbName: "Feather Token (Chest)", foundryName: "Marvelous Miniatures (Chest)" },
  444. { pbName: "Feather Token (Ladder)", foundryName: "Marvelous Miniatures (Ladder)" },
  445. { pbName: "Feather Token (Swan Boat)", foundryName: "Marvelous Miniatures (Boat)" },
  446. { pbName: "Flame Tongue", foundryName: "Searing Blade" },
  447. { pbName: "Gloves of Storing", foundryName: "Retrieval Belt" },
  448. { pbName: "Goggles of Night", foundryName: "Obsidian Goggles" },
  449. { pbName: "Goggles of Night (Greater)", foundryName: "Obsidian Goggles (Greater)" },
  450. { pbName: "Goggles of Night (Major)", foundryName: "Obsidian Goggles (Major)" },
  451. { pbName: "Hat of Disguise", foundryName: "Masquerade Scarf" },
  452. { pbName: "Hat of Disguise (Greater)", foundryName: "Masquerade Scarf (Greater)" },
  453. { pbName: "Horn of Fog", foundryName: "Cloud Pouch" },
  454. { pbName: "Horseshoes of Speed", foundryName: "Alacritous Horseshoes" },
  455. { pbName: "Javelin of Lightning", foundryName: "Trident of Lightning" },
  456. { pbName: "Potion of Expeditious Retreat", foundryName: "Potion of Emergency Escape" },
  457. { pbName: "Ring of Energy Resistance (Greater)", foundryName: "Charm of Resistance (Greater)" },
  458. { pbName: "Ring of Energy Resistance (Major)", foundryName: "Charm of Resistance (Major)" },
  459. { pbName: "Silversheen", foundryName: "Silver Salve" },
  460. { pbName: "Smokestick (Lesser)", foundryName: "Smoke Ball (Lesser)" },
  461. { pbName: "Smokestick (Greater)", foundryName: "Smoke Ball (Greater)" },
  462. { pbName: "Sunrod", foundryName: "Glow Rod" },
  463. { pbName: "Tanglefoot Bag (Lesser)", foundryName: "Glue Bomb (Lesser)" },
  464. { pbName: "Tanglefoot Bag (Moderate)", foundryName: "Glue Bomb (Moderate)" },
  465. { pbName: "Tanglefoot Bag (Major)", foundryName: "Glue Bomb (Major)" },
  466. { pbName: "Tanglefoot Bag (Greater)", foundryName: "Glue Bomb (Greater)" },
  467. { pbName: "Tindertwig", foundryName: "Matchstick" },
  468. { pbName: "Owlbear Claw", foundryName: "Predator's Claw" },
  469. { pbName: "Wand of Manifold Missiles", foundryName: "Wand of Shardstorm" },
  470. { pbName: "Wand of Manifold Missiles (1st-Level Spell)", foundryName: "Wand of Shardstorm (1st-Rank Spell)" },
  471. { pbName: "Wand of Manifold Missiles (3rd-Level Spell)", foundryName: "Wand of Shardstorm (3rd-Rank Spell)" },
  472. { pbName: "Wand of Manifold Missiles (5th-Level Spell)", foundryName: "Wand of Shardstorm (5th-Rank Spell)" },
  473. { pbName: "Wand of Manifold Missiles (7th-Level Spell)", foundryName: "Wand of Shardstorm (7th-Rank Spell)" },
  474. ];
  475. function dynamicNamesSteps(pbName) {
  476. const result = [];
  477. for (const reg of POSTFIX_PB_REMOVALS) {
  478. const match = pbName.match(reg);
  479. if (match) {
  480. result.push({ pbName, foundryName: match[1], details: match[2] });
  481. }
  482. }
  483. for (const reg of SWAPS) {
  484. const match = pbName.match(reg);
  485. if (match) {
  486. result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] });
  487. }
  488. }
  489. for (const reg of SPLITS) {
  490. const match = pbName.match(reg);
  491. if (match) {
  492. result.push({ pbName, foundryName: match[2], details: match[1] });
  493. }
  494. }
  495. for (const reg of SPLITS_INVERT) {
  496. const match = pbName.match(reg);
  497. if (match) {
  498. result.push({ pbName, foundryName: match[1], details: match[2] });
  499. }
  500. }
  501. for (const reg of PARENTHESIS) {
  502. const match = pbName.match(reg);
  503. if (match) {
  504. result.push({ pbName, foundryName: match[1], details: match[2] });
  505. }
  506. }
  507. return result;
  508. }
  509. function generateDynamicNames(pbName) {
  510. const result = [];
  511. // if we have a hardcoded map, don't return here
  512. const basicResults = EQUIPMENT_RENAME_STATIC_MAP.filter((e) => e.pbName === pbName);
  513. if (basicResults.length > 0) {
  514. result.push(...basicResults);
  515. }
  516. for (const replace of REPLACES) {
  517. if (pbName.includes(replace.pbName)) {
  518. const replaced = pbName.replace(replace.pbName, replace.foundryName);
  519. result.push(...dynamicNamesSteps(replaced));
  520. result.push({ pbName, foundryName: replaced });
  521. }
  522. }
  523. if (result.length > 0) {
  524. return result;
  525. }
  526. result.push(...dynamicNamesSteps(pbName));
  527. return result;
  528. }
  529. function EQUIPMENT_RENAME_MAP(pbName = null) {
  530. const postfixNames = pbName ? generateDynamicNames(pbName) : [];
  531. return postfixNames.concat(EQUIPMENT_RENAME_STATIC_MAP);
  532. }
  533. // this is equipment is special and shouldn't have the transformations applied to it
  534. const RESTRICTED_EQUIPMENT = [
  535. "Bracers of Armor",
  536. ];
  537. const IGNORED_EQUIPMENT = [
  538. "Unarmored",
  539. "Inventor Power Suit",
  540. "Inventor Power Suit (Heavy)",
  541. "Inventor Subterfuge Suit",
  542. ];
  543. const IGNORED_DISPLAY_POSTFIX = [
  544. /(.*) - Melee$/,
  545. /(.*) - Ranged$/,
  546. ];
  547. function IGNORED_EQUIPMENT_DISPLAY(pbName) {
  548. for (const reg of IGNORED_DISPLAY_POSTFIX) {
  549. const match = reg.test(pbName);
  550. if (match === true) return true;
  551. }
  552. return false;
  553. }
  554. const GRANTED_ITEMS_LIST = [
  555. "Inventor Power Suit",
  556. "Inventor Power Suit (Heavy)",
  557. "Inventor Subterfuge Suit",
  558. ];
  559. ;// CONCATENATED MODULE: ./src/data/features.js
  560. // these are features which are named differently in pathbuilder to foundry
  561. const SKILL_LOOKUP = {
  562. "acrobatics": "acr",
  563. "arcana": "arc",
  564. "athletics": "ath",
  565. "crafting": "cra",
  566. "deception": "dec",
  567. "diplomacy": "dip",
  568. "intimidation": "itm",
  569. "medicine": "med",
  570. "nature": "nat",
  571. "occultism": "occ",
  572. "performance": "prf",
  573. "religion": "rel",
  574. "society": "soc",
  575. "stealth": "ste",
  576. "survival": "sur",
  577. "thievery": "thi",
  578. };
  579. const features_POSTFIX_PB_REMOVALS = [
  580. /(.*) (Racket)$/,
  581. /(.*) (Style)$/,
  582. /(.*) (Initiate Benefit)$/,
  583. /(.*) Mystery$/,
  584. // Cleric +
  585. /(.*) (Doctrine)$/,
  586. /(.*) (Element)$/,
  587. /(.*) (Impulse Junction)$/,
  588. /(.*) (Gate Junction:).*$/,
  589. /(.*) (Patron)$/,
  590. ];
  591. const PREFIX_PB_REMOVALS = [
  592. /^(Arcane Thesis): (.*)/,
  593. /^(Arcane School): (.*)/,
  594. /^(The) (.*)/,
  595. // Cleric +
  596. /^(Blessing): (.*)/,
  597. /^(Empiricism) Selected Skill: (.*)/,
  598. ];
  599. const POSTFIX_PB_SPLIT_AND_KEEP = [
  600. /(.*) (Impulse Junction)$/,
  601. /(.*) Gate Junction: (.*)$/,
  602. ];
  603. const features_PARENTHESIS = [
  604. /^(.*) \((.*)\)$/,
  605. ];
  606. const features_SPLITS = [
  607. /^(.*): (.*)/,
  608. ];
  609. const features_SWAPS = [
  610. /^(Greater) (.*)/,
  611. /^(Lesser) (.*)/,
  612. /^(Major) (.*)/,
  613. /^(Moderate) (.*)/,
  614. /^(Standard) (.*)/,
  615. ];
  616. const FEAT_RENAME_STATIC_MAP = [
  617. { pbName: "Academic", foundryName: "Ustalavic Academic" },
  618. { pbName: "Academic (Arcana)", foundryName: "Magaambya Academic" },
  619. { pbName: "Academic (Nature)", foundryName: "Magaambya Academic" },
  620. { pbName: "Aerialist", foundryName: "Shory Aerialist" },
  621. { pbName: "Aeromancer", foundryName: "Shory Aeromancer" },
  622. { pbName: "Ancient-Blooded", foundryName: "Ancient-Blooded Dwarf" },
  623. { pbName: "Antipaladin [Chaotic Evil]", foundryName: "Antipaladin" },
  624. { pbName: "Ape", foundryName: "Ape Animal Instinct" },
  625. { pbName: "Aquatic Eyes (Darkvision)", foundryName: "Aquatic Eyes" },
  626. { pbName: "Astrology", foundryName: "Saoc Astrology" },
  627. { pbName: "Battle Ready", foundryName: "Battle-Ready Orc" },
  628. { pbName: "Bite (Gnoll)", foundryName: "Bite" },
  629. { pbName: "Bloodline: Genie (Efreeti)", foundryName: "Bloodline: Genie" },
  630. { pbName: "Bloody Debilitations", foundryName: "Bloody Debilitation" },
  631. { pbName: "Canoneer", foundryName: "Cannoneer" },
  632. { pbName: "Cave Climber Kobold", foundryName: "Caveclimber Kobold" },
  633. { pbName: "Child of Squalor", foundryName: "Child of the Puddles" },
  634. { pbName: "Chosen One", foundryName: "Chosen of Lamashtu" },
  635. { pbName: "Cognative Mutagen (Greater)", foundryName: "Cognitive Mutagen (Greater)" },
  636. { pbName: "Cognative Mutagen (Lesser)", foundryName: "Cognitive Mutagen (Lesser)" },
  637. { pbName: "Cognative Mutagen (Major)", foundryName: "Cognitive Mutagen (Major)" },
  638. { pbName: "Cognative Mutagen (Moderate)", foundryName: "Cognitive Mutagen (Moderate)" },
  639. { pbName: "Cognitive Crossover", foundryName: "Kreighton's Cognitive Crossover" },
  640. { pbName: "Collegiate Attendant Dedication", foundryName: "Magaambyan Attendant Dedication" },
  641. { pbName: "Construct Carver", foundryName: "Tupilaq Carver" },
  642. { pbName: "Cunning Stance", foundryName: "Devrin's Cunning Stance" },
  643. { pbName: "Constructed (Android)", foundryName: "Constructed" },
  644. { pbName: "Dazzling Diversion", foundryName: "Devrin's Dazzling Diversion" },
  645. { pbName: "Deadly Hair", foundryName: "Syu Tak-nwa's Deadly Hair" },
  646. { pbName: "Deepvision", foundryName: "Deep Vision" },
  647. { pbName: "Deflect Arrows", foundryName: "Deflect Arrow" },
  648. { pbName: "Desecrator [Neutral Evil]", foundryName: "Desecrator" },
  649. { pbName: "Detective Dedication", foundryName: "Edgewatch Detective Dedication" },
  650. { pbName: "Duelist Dedication (LO)", foundryName: "Aldori Duelist Dedication" },
  651. { pbName: "Dwarven Hold Education", foundryName: "Dongun Education" },
  652. { pbName: "Ember's Eyes (Darkvision)", foundryName: "Ember's Eyes" },
  653. { pbName: "Enhanced Familiar Feat", foundryName: "Enhanced Familiar" },
  654. { pbName: "Enhanced Fire", foundryName: "Artokus's Fire" },
  655. { pbName: "Enigma", foundryName: "Enigma Muse" },
  656. { pbName: "Escape", foundryName: "Fane's Escape" },
  657. { pbName: "Eye of the Arcane Lords", foundryName: "Eye of the Arclords" },
  658. { pbName: "Flip", foundryName: "Farabellus Flip" },
  659. { pbName: "Fourberie", foundryName: "Fane's Fourberie" },
  660. { pbName: "Ganzi Gaze (Low-Light Vision)", foundryName: "Ganzi Gaze" },
  661. { pbName: "Guild Agent Dedication", foundryName: "Pathfinder Agent Dedication" },
  662. { pbName: "Harmful Font", foundryName: "Divine Font" },
  663. { pbName: "Green Watcher", foundryName: "Greenwatcher" },
  664. { pbName: "Green Watch Initiate", foundryName: "Greenwatch Initiate" },
  665. { pbName: "Green Watch Veteran", foundryName: "Greenwatch Veteran" },
  666. { pbName: "Healing Font", foundryName: "Divine Font" },
  667. { pbName: "Heatwave", foundryName: "Heat Wave" },
  668. { pbName: "Heavenseeker Dedication", foundryName: "Jalmeri Heavenseeker Dedication" },
  669. { pbName: "Heir of the Astrologers", foundryName: "Heir of the Saoc" },
  670. { pbName: "High Killer Training", foundryName: "Vernai Training" },
  671. { pbName: "Ice-Witch", foundryName: "Irriseni Ice-Witch" },
  672. { pbName: "Impeccable Crafter", foundryName: "Impeccable Crafting" },
  673. { pbName: "Incredible Beastmaster's Companion", foundryName: "Incredible Beastmaster Companion" },
  674. { pbName: "Interrogation", foundryName: "Bolera's Interrogation" },
  675. { pbName: "Katana", foundryName: "Katana Weapon Familiarity" },
  676. { pbName: "Liberator [Chaotic Good]", foundryName: "Liberator" },
  677. { pbName: "Lumberjack Dedication", foundryName: "Turpin Rowe Lumberjack Dedication" },
  678. { pbName: "Lumberjack", foundryName: "Lumber Consortium Laborer" },
  679. { pbName: "Maestro", foundryName: "Maestro Muse" },
  680. { pbName: "Major Lesson I", foundryName: "Major Lesson" },
  681. { pbName: "Major Lesson II", foundryName: "Major Lesson" },
  682. { pbName: "Major Lesson III", foundryName: "Major Lesson" },
  683. { pbName: "Mantis God's Grip", foundryName: "Achaekek's Grip" },
  684. { pbName: "Marked for Death", foundryName: "Mark for Death" },
  685. { pbName: "Miraculous Spells", foundryName: "Miraculous Spell" },
  686. { pbName: "Multifarious", foundryName: "Multifarious Muse" },
  687. { pbName: "Mystic", foundryName: "Nexian Mystic" },
  688. { pbName: "Paladin [Lawful Good]", foundryName: "Paladin" },
  689. { pbName: "Parry", foundryName: "Aldori Parry" },
  690. { pbName: "Polymath", foundryName: "Polymath Muse" },
  691. { pbName: "Precise Debilitation", foundryName: "Precise Debilitations" },
  692. { pbName: "Prodigy", foundryName: "Merabite Prodigy" },
  693. { pbName: "Quick Climber", foundryName: "Quick Climb" },
  694. { pbName: "Raider", foundryName: "Ulfen Raider" },
  695. { pbName: "Recognise Threat", foundryName: "Recognize Threat" },
  696. { pbName: "Redeemer [Neutral Good]", foundryName: "Redeemer" },
  697. { pbName: "Revivification Protocall", foundryName: "Revivification Protocol" },
  698. { pbName: "Riposte", foundryName: "Aldori Riposte" },
  699. { pbName: "Rkoan Arts", foundryName: "Rokoan Arts" },
  700. { pbName: "Saberteeth", foundryName: "Saber Teeth" },
  701. { pbName: "Scholarly Recollection", foundryName: "Uzunjati Recollection" },
  702. { pbName: "Scholarly Storytelling", foundryName: "Uzunjati Storytelling" },
  703. { pbName: "Shamanic Adherent", foundryName: "Rivethun Adherent" },
  704. { pbName: "Shamanic Disciple", foundryName: "Rivethun Disciple" },
  705. { pbName: "Shamanic Spiritual Attunement", foundryName: "Rivethun Spiritual Attunement" },
  706. { pbName: "Skysage Dedication", foundryName: "Oatia Skysage Dedication" },
  707. { pbName: "Secret Lesson", foundryName: "Janatimo's Lessons" },
  708. { pbName: "Sentry Dedication", foundryName: "Lastwall Sentry Dedication" },
  709. { pbName: "Stab and Snag", foundryName: "Stella's Stab and Snag" },
  710. { pbName: "Tenets of Evil", foundryName: "The Tenets of Evil" },
  711. { pbName: "Tenets of Good", foundryName: "The Tenets of Good" },
  712. { pbName: "Tongue of the Sun and Moon", foundryName: "Tongue of Sun and Moon" },
  713. { pbName: "Tribal Bond", foundryName: "Quah Bond" },
  714. { pbName: "Tyrant [Lawful Evil]", foundryName: "Tyrant" },
  715. { pbName: "Vestigal Wings", foundryName: "Vestigial Wings" },
  716. { pbName: "Virtue-Forged Tattooed", foundryName: "Virtue-Forged Tattoos" },
  717. { pbName: "Wakizashi", foundryName: "Wakizashi Weapon Familiarity" },
  718. { pbName: "Warden", foundryName: "Lastwall Warden" },
  719. { pbName: "Warrior", foundryName: "Warrior Muse" },
  720. { pbName: "Wary Eye", foundryName: "Eye of Ozem" },
  721. { pbName: "Wayfinder Resonance Infiltrator", foundryName: "Westyr's Wayfinder Repository" },
  722. { pbName: "Wind God's Fan", foundryName: "Wind God’s Fan" },
  723. { pbName: "Wind God’s Fan", foundryName: "Wind God's Fan" },
  724. // dragons
  725. { pbName: "Black", foundryName: "Black Dragon" },
  726. { pbName: "Brine", foundryName: "Brine Dragon" },
  727. { pbName: "Copper", foundryName: "Copper Dragon" },
  728. { pbName: "Blue", foundryName: "Blue Dragon" },
  729. { pbName: "Bronze", foundryName: "Bronze Dragon" },
  730. { pbName: "Cloud", foundryName: "Cloud Dragon" },
  731. { pbName: "Sky", foundryName: "Sky Dragon" },
  732. { pbName: "Brass", foundryName: "Brass Dragon" },
  733. { pbName: "Underworld", foundryName: "Underworld Dragon" },
  734. { pbName: "Crystal", foundryName: "Crystal Dragon" },
  735. { pbName: "Forest", foundryName: "Forest Dragon" },
  736. { pbName: "Green", foundryName: "Green Dragon" },
  737. { pbName: "Sea", foundryName: "Sea Dragon" },
  738. { pbName: "Silver", foundryName: "Silver Dragon" },
  739. { pbName: "White", foundryName: "White Dragon" },
  740. { pbName: "Sovereign", foundryName: "Sovereign Dragon" },
  741. { pbName: "Umbral", foundryName: "Umbral Dragon" },
  742. { pbName: "Red", foundryName: "Red Dragon" },
  743. { pbName: "Gold", foundryName: "Gold Dragon" },
  744. { pbName: "Magma", foundryName: "Magma Dragon" },
  745. // sizes for fleshwarp
  746. { pbName: "Medium", foundryName: "med" },
  747. { pbName: "Small", foundryName: "sm" },
  748. // Cleric +
  749. { pbName: "Decree of the Warsworn Ecstacy", foundryName: "Decree of Warsworn Ecstacy" },
  750. { pbName: "Decree of Warsworn Ecstacy", foundryName: "Decree of the Warsworn Ecstacy" },
  751. // remaster
  752. { pbName: "Lightning Reflexes", foundryName: "Reflex Expertise" },
  753. { pbName: "Great Fortitude", foundryName: "Fortitude Expertise" },
  754. { pbName: "Iron Will", foundryName: "Will Expertise" },
  755. { pbName: "Alertness", foundryName: "Perception Expertise" },
  756. { pbName: "Incredible Senses", foundryName: "Perception Legend" },
  757. { pbName: "Vigilant Senses", foundryName: "Perception Mastery" },
  758. ];
  759. function features_generateDynamicNames(pbName) {
  760. const result = [];
  761. // if we have a hardcoded map, don't return here
  762. if (FEAT_RENAME_STATIC_MAP.some((e) => e.pbName === pbName)) return result;
  763. for (const reg of features_POSTFIX_PB_REMOVALS) {
  764. const match = pbName.match(reg);
  765. if (match) {
  766. result.push({ pbName, foundryName: match[1], details: match[2] });
  767. }
  768. }
  769. for (const reg of PREFIX_PB_REMOVALS) {
  770. const match = pbName.match(reg);
  771. if (match) {
  772. const parsed = { pbName, foundryName: match[2], details: match[1] };
  773. parsed.foundryValue = SKILL_LOOKUP[parsed.foundryName.toLowerCase()];
  774. result.push(parsed);
  775. }
  776. }
  777. for (const reg of features_SPLITS) {
  778. const match = pbName.match(reg);
  779. if (match) {
  780. result.push({ pbName, foundryName: match[2], details: match[1] });
  781. }
  782. }
  783. for (const reg of features_PARENTHESIS) {
  784. const match = pbName.match(reg);
  785. if (match) {
  786. result.push({ pbName, foundryName: match[1], details: match[2] });
  787. }
  788. }
  789. for (const reg of features_SWAPS) {
  790. const match = pbName.match(reg);
  791. if (match) {
  792. result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] });
  793. }
  794. }
  795. return result;
  796. }
  797. function FEAT_RENAME_MAP(pbName = null) {
  798. const postfixNames = pbName ? features_generateDynamicNames(pbName) : [];
  799. return postfixNames.concat(FEAT_RENAME_STATIC_MAP);
  800. }
  801. const SHARED_IGNORE_LIST = [
  802. "Draconic Rage", // just handled by effects on Draconic Instinct
  803. "Mirror Initiate Benefit",
  804. "Spellstrike Specifics",
  805. "Unarmored",
  806. "Simple Weapon Expertise",
  807. "Spellbook",
  808. "Titan Mauler", // not needed
  809. "Energy Emanation", // pathbuilder does not pass through a type for this
  810. "Imprecise Sense", // this gets picked up and added by granted features
  811. "Imprecise Scent", // this gets picked up and added by granted features
  812. "Sanctification", // choose on import
  813. ];
  814. const IGNORED_FEATS_LIST = [
  815. // ignore skills listed as feats
  816. "Acrobatics",
  817. "Athletics",
  818. "Deception",
  819. "Intimidation",
  820. "Nature",
  821. "Performance",
  822. "Society",
  823. "Survival",
  824. "Arcana",
  825. "Crafting",
  826. "Diplomacy",
  827. "Medicine",
  828. "Occultism",
  829. "Religion",
  830. "Stealth",
  831. "Thievery",
  832. // sizes
  833. // "Medium",
  834. // "Small",
  835. ];
  836. const IGNORED_SPECIALS_LIST = [
  837. ];
  838. function IGNORED_FEATS() {
  839. // const visionFeats = utils.setting("ADD_VISION_FEATS") ? [] : ["Low-Light Vision", "Darkvision"];
  840. return IGNORED_FEATS_LIST.concat(SHARED_IGNORE_LIST);
  841. }
  842. function IGNORED_SPECIALS() {
  843. const visionFeats = src_utils.setting("ADD_VISION_FEATS") ? [] : ["Low-Light Vision", "Darkvision"];
  844. return IGNORED_SPECIALS_LIST.concat(SHARED_IGNORE_LIST, visionFeats);
  845. }
  846. function specialOnlyNameLookup(name) {
  847. for (const [key, value] of Object.entries(SKILL_LOOKUP)) {
  848. if (key === name.toLowerCase()) {
  849. return { pbName: name, foundryName: name, foundryValue: value };
  850. }
  851. }
  852. return undefined;
  853. }
  854. function SPECIAL_NAME_ADDITIONS(specials) {
  855. const newSpecials = [];
  856. for (const special of specials) {
  857. for (const reg of POSTFIX_PB_SPLIT_AND_KEEP) {
  858. const match = special.match(reg);
  859. if (match) {
  860. newSpecials.push(match[2]);
  861. }
  862. }
  863. }
  864. return newSpecials;
  865. }
  866. const NO_AUTO_CHOICE_LIST = [
  867. // "Elemental Evolution",
  868. ];
  869. function NO_AUTO_CHOICE() {
  870. return NO_AUTO_CHOICE_LIST;
  871. }
  872. ;// CONCATENATED MODULE: ./src/data/spells.js
  873. const FEAT_SPELLCASTING = [
  874. { name: "Kitsune Spell Familiarity", showSlotless: false, knownSpells: ["Daze", "Forbidding Ward", "Ghost Sound"], preparePBSpells: true, },
  875. { name: "Kitsune Spell Expertise", showSlotless: false, knownSpells: ["Confusion", "Death Ward", "Illusory Scene"], preparePBSpells: true, },
  876. { name: "Kitsune Spell Mysteries", showSlotless: false, knownSpells: ["Bane", "Illusory Object", "Sanctuary"], preparePBSpells: true, },
  877. { name: "Nagaji Spell Familiarity", showSlotless: false, knownSpells: ["Daze", "Detect Magic", "Mage Hand"], preparePBSpells: true, },
  878. { name: "Nagaji Spell Expertise", showSlotless: false, knownSpells: ["Blink", "Control Water", "Subconscious Suggestion"], preparePBSpells: true, },
  879. { name: "Nagaji Spell Mysteries", showSlotless: false, knownSpells: ["Charm", "Fleet Step", "Heal"], preparePBSpells: true, },
  880. { name: "Rat Magic", showSlotless: false, knownSpells: [], preparePBSpells: true, },
  881. ];
  882. const REMASTER_NAMES = [
  883. { pbName: "Animate Dead", foundryName: "Summon Undead" },
  884. { pbName: "Augment Summoning", foundryName: "Fortify Summoning" },
  885. { pbName: "Baleful Polymorph", foundryName: "Cursed Metamorphosis" },
  886. { pbName: "Barkskin", foundryName: "Oaken Resilience" },
  887. { pbName: "Bind Soul", foundryName: "Seize Soul" },
  888. { pbName: "Blind Ambition", foundryName: "Ignite Ambition" },
  889. { pbName: "Blink", foundryName: "Flicker" },
  890. { pbName: "Burning Hands", foundryName: "Breathe Fire" },
  891. { pbName: "Calm Emotions", foundryName: "Calm" },
  892. { pbName: "Charming Words", foundryName: "Charming Push" },
  893. { pbName: "Chill Touch", foundryName: "Void Warp" },
  894. { pbName: "Cloudkill", foundryName: "Toxic Cloud" },
  895. { pbName: "Color Spray", foundryName: "Dizzying Colors" },
  896. { pbName: "Commune with Nature", foundryName: "Commune" },
  897. { pbName: "Comprehend Language", foundryName: "Translate" },
  898. { pbName: "Continual Flame", foundryName: "Everlight" },
  899. { pbName: "Crushing Despair", foundryName: "Wave of Despair" },
  900. { pbName: "Dancing Lights", foundryName: "Light" },
  901. { pbName: "Dimension Door", foundryName: "Translocate" },
  902. { pbName: "Dimensional Anchor", foundryName: "Planar Seal" },
  903. { pbName: "Dimensional Lock", foundryName: "Planar Tether" },
  904. { pbName: "Discern Location", foundryName: "Pinpoint" },
  905. { pbName: "Disrupt Undead", foundryName: "Vitality Lash" },
  906. { pbName: "Disrupting Weapons", foundryName: "Infuse Vitality" },
  907. { pbName: "Endure Elements", foundryName: "Environmental Endurance" },
  908. { pbName: "Entangle", foundryName: "Entangling Flora" },
  909. { pbName: "False Life", foundryName: "False Vitality" },
  910. { pbName: "Feather Fall", foundryName: "Gentle Landing" },
  911. { pbName: "Feeblemind", foundryName: "Never Mind" },
  912. { pbName: "Finger of Death", foundryName: "Execute" },
  913. { pbName: "Flaming Sphere", foundryName: "Floating Flame" },
  914. { pbName: "Flesh To Stone", foundryName: "Petrify" },
  915. { pbName: "Freedom of Movement", foundryName: "Unfettered Movement" },
  916. { pbName: "Gaseous Form", foundryName: "Vapor Form" },
  917. { pbName: "Gentle Repose", foundryName: "Peaceful Rest" },
  918. { pbName: "Glibness", foundryName: "Honeyed Words" },
  919. { pbName: "Glitterdust", foundryName: "Revealing Light" },
  920. { pbName: "Globe of Invulnerability", foundryName: "Dispelling Globe" },
  921. { pbName: "Hallucinatory Terrain", foundryName: "Mirage" },
  922. { pbName: "Hideous Laughter", foundryName: "Laughing Fit" },
  923. { pbName: "Horrid Wilting", foundryName: "Desiccate" },
  924. { pbName: "Hypnotic Pattern", foundryName: "Hypnotize" },
  925. { pbName: "Inspire Competence", foundryName: "Uplifting Overture" },
  926. { pbName: "Inspire Courage", foundryName: "Courageous Anthem" },
  927. { pbName: "Inspire Defense", foundryName: "Rallying Anthem" },
  928. { pbName: "Inspire Heroics", foundryName: "Fortissimo Composition" },
  929. { pbName: "Know Direction", foundryName: "Know the Way" },
  930. { pbName: "Legend Lore", foundryName: "Collective Memories" },
  931. { pbName: "Longstrider", foundryName: "Tailwind" },
  932. { pbName: "Mage Armor", foundryName: "Mystic Armor" },
  933. { pbName: "Mage Hand", foundryName: "Telekinetic Hand" },
  934. { pbName: "Magic Aura", foundryName: "Disguise Magic" },
  935. { pbName: "Magic Fang", foundryName: "Runic Body" },
  936. { pbName: "Magic Missile", foundryName: "Force Barrage" },
  937. { pbName: "Magic Mouth", foundryName: "Embed Message" },
  938. { pbName: "Magic Weapon", foundryName: "Runic Weapon" },
  939. { pbName: "Magnificent Mansion", foundryName: "Planar Palace" },
  940. { pbName: "Maze", foundryName: "Quandary" },
  941. { pbName: "Meld into Stone", foundryName: "One with Stone" },
  942. { pbName: "Meteor Swarm", foundryName: "Falling Stars" },
  943. { pbName: "Mind Blank", foundryName: "Hidden Mind" },
  944. { pbName: "Misdirection", foundryName: "Disguise Magic" },
  945. { pbName: "Modify Memory", foundryName: "Rewrite Memory" },
  946. { pbName: "Neutralize Poison", foundryName: "Cleanse Affliction" },
  947. { pbName: "Nondetection", foundryName: "Veil of Privacy" },
  948. { pbName: "Obscuring Mist", foundryName: "Mist" },
  949. { pbName: "Pass Without Trace", foundryName: "Vanishing Tracks" },
  950. { pbName: "Passwall", foundryName: "Magic Passage" },
  951. { pbName: "Phantom Mount", foundryName: "Marvelous Mount" },
  952. { pbName: "Planar Binding", foundryName: "Planar Servitor" },
  953. { pbName: "Plane Shift", foundryName: "Interplanar Teleport" },
  954. { pbName: "Positive Luminance", foundryName: "Vital Luminance" },
  955. { pbName: "Private Sanctum", foundryName: "Peaceful Bubble" },
  956. { pbName: "Prying Eye", foundryName: "Scouting Eye" },
  957. { pbName: "Pulse of The City", foundryName: "Pulse of Civilization" },
  958. { pbName: "Purify Food And Drink", foundryName: "Cleanse Cuisine" },
  959. { pbName: "Ray of Enfeeblement", foundryName: "Enfeeble" },
  960. { pbName: "Remove Curse", foundryName: "Cleanse Affliction" },
  961. { pbName: "Remove Disease", foundryName: "Cleanse Affliction" },
  962. { pbName: "Remove Fear", foundryName: "Clear Mind" },
  963. { pbName: "Remove Paralysis", foundryName: "Sure Footing" },
  964. { pbName: "Restore Senses", foundryName: "Sound Body" },
  965. { pbName: "Scorching Ray", foundryName: "Blazing Bolt" },
  966. { pbName: "Searing Light", foundryName: "Holy Light" },
  967. { pbName: "See Invisibility", foundryName: "See the Unseen" },
  968. { pbName: "Shadow Walk", foundryName: "Umbral Journey" },
  969. { pbName: "Shapechange", foundryName: "Metamorphosis" },
  970. { pbName: "Shield Other", foundryName: "Share Life" },
  971. { pbName: "Sound Burst", foundryName: "Noise Blast" },
  972. { pbName: "Spectral Hand", foundryName: "Ghostly Carrier" },
  973. { pbName: "Spider Climb", foundryName: "Gecko Grip" },
  974. { pbName: "Splash of Art", foundryName: "Creative Splash" },
  975. { pbName: "Stone Tell", foundryName: "Speak with Stones" },
  976. { pbName: "Stoneskin", foundryName: "Mountain Resilience" },
  977. { pbName: "Tanglefoot", foundryName: "Tangle Vine" },
  978. { pbName: "Time Stop", foundryName: "Freeze Time" },
  979. { pbName: "Tongues", foundryName: "Truespeech" },
  980. { pbName: "Touch of Idiocy", foundryName: "Stupefy" },
  981. { pbName: "Tree Shape", foundryName: "One with Plants" },
  982. { pbName: "Tree Stride", foundryName: "Nature's Pathway" },
  983. { pbName: "Trueseeing", foundryName: "Truesight" },
  984. { pbName: "True Strike", foundryName: "Sure Strike" },
  985. { pbName: "Unseen Servant", foundryName: "Phantasmal Minion" },
  986. { pbName: "Vampiric Touch", foundryName: "Vampiric Feast" },
  987. { pbName: "Veil", foundryName: "Illusory Disguise" },
  988. { pbName: "Vigilant Eye", foundryName: "Rune of Observation" },
  989. { pbName: "Wail of the Banshee", foundryName: "Wails of the Damned" },
  990. { pbName: "Wind Walk", foundryName: "Migration" },
  991. { pbName: "Zone of Truth", foundryName: "Ring of Truth" },
  992. ];
  993. function spellRename(spellName) {
  994. if (isNewerVersion(game.version, "5.9.0") && game.modules.get("pf2e-legacy-content")?.active) return spellName;
  995. const remasterName = REMASTER_NAMES.find((remaster) => remaster.pbName === spellName);
  996. if (remasterName) {
  997. return remasterName.foundryName;
  998. }
  999. return spellName;
  1000. }
  1001. ;// CONCATENATED MODULE: ./src/app/Seasoning.js
  1002. /**
  1003. * This class acts as a wrapper around the renaming data,
  1004. * and the changing of names for foundry
  1005. *
  1006. * When Munching we refer to this as Seasoning the data to taste.
  1007. *
  1008. * It's split out just to make it more manageable
  1009. */
  1010. class Seasoning {
  1011. // sluggify
  1012. static slug(name) {
  1013. if (!name) return undefined;
  1014. return game.pf2e.system.sluggify(name);
  1015. }
  1016. // sluggify with dromedary casing
  1017. static slugD(name) {
  1018. if (!name) return undefined;
  1019. return game.pf2e.system.sluggify(name, { camel: "dromedary" });
  1020. }
  1021. static FEAT_RENAME_MAP(name) {
  1022. return FEAT_RENAME_MAP(name);
  1023. }
  1024. static EQUIPMENT_RENAME_MAP(name) {
  1025. return EQUIPMENT_RENAME_MAP(name);
  1026. }
  1027. static getSpellCastingFeatureAdjustment(name) {
  1028. return FEAT_SPELLCASTING.find((f) => f.name === name);
  1029. }
  1030. static getFoundryEquipmentName(pbName) {
  1031. return Seasoning.EQUIPMENT_RENAME_MAP(pbName).find((map) => map.pbName == pbName)?.foundryName ?? pbName;
  1032. }
  1033. // static getFoundryFeatureName(pbName) {
  1034. // const match = Seasoning.FEAT_RENAME_MAP(pbName).find((map) => map.pbName == pbName);
  1035. // return match ?? { pbName, foundryName: pbName, details: undefined };
  1036. // }
  1037. static RESTRICTED_EQUIPMENT() {
  1038. return RESTRICTED_EQUIPMENT;
  1039. }
  1040. // specials that are handled by Foundry and shouldn't be added
  1041. static IGNORED_FEATS() {
  1042. return IGNORED_FEATS();
  1043. };
  1044. static IGNORED_SPECIALS() {
  1045. return IGNORED_SPECIALS();
  1046. }
  1047. static IGNORED_EQUIPMENT() {
  1048. return IGNORED_EQUIPMENT;
  1049. };
  1050. static IGNORED_EQUIPMENT_DISPLAY(pbName) {
  1051. return IGNORED_EQUIPMENT_DISPLAY(pbName);
  1052. }
  1053. static GRANTED_ITEMS_LIST() {
  1054. return GRANTED_ITEMS_LIST;
  1055. }
  1056. static getSizeValue(size) {
  1057. switch (size) {
  1058. case 0:
  1059. return "tiny";
  1060. case 1:
  1061. return "sm";
  1062. case 3:
  1063. return "lg";
  1064. default:
  1065. return "med";
  1066. }
  1067. }
  1068. static PHYSICAL_ITEM_TYPES = new Set([
  1069. "armor",
  1070. "backpack",
  1071. "book",
  1072. "consumable",
  1073. "equipment",
  1074. "treasure",
  1075. "weapon"
  1076. ]);
  1077. static isPhysicalItemType(type) {
  1078. return Seasoning.PHYSICAL_ITEM_TYPES.has(type);
  1079. }
  1080. static getMaterialGrade(material) {
  1081. if (material.toLowerCase().includes("high-grade")) {
  1082. return "high";
  1083. } else if (material.toLowerCase().includes("standard-grade")) {
  1084. return "standard";
  1085. }
  1086. return "low";
  1087. }
  1088. static getFoundryFeatLocation(pathbuilderFeatType, pathbuilderFeatLevel) {
  1089. if (pathbuilderFeatType === "Ancestry Feat") {
  1090. return `ancestry-${pathbuilderFeatLevel}`;
  1091. } else if (pathbuilderFeatType === "Class Feat") {
  1092. return `class-${pathbuilderFeatLevel}`;
  1093. } else if (pathbuilderFeatType === "Skill Feat") {
  1094. return `skill-${pathbuilderFeatLevel}`;
  1095. } else if (pathbuilderFeatType === "General Feat") {
  1096. return `general-${pathbuilderFeatLevel}`;
  1097. } else if (pathbuilderFeatType === "Background Feat") {
  1098. return `skill-${pathbuilderFeatLevel}`;
  1099. } else if (pathbuilderFeatType === "Archetype Feat") {
  1100. return `archetype-${pathbuilderFeatLevel}`;
  1101. } else if (pathbuilderFeatType === "Kineticist Feat") { // return as null for now
  1102. return null;
  1103. } else {
  1104. return null;
  1105. }
  1106. }
  1107. static getClassAdjustedSpecialNameLowerCase(name, className) {
  1108. return `${name} (${className})`.toLowerCase();
  1109. }
  1110. static getDualClassAdjustedSpecialNameLowerCase(name, dualClassName) {
  1111. return `${name} (${dualClassName})`.toLowerCase();
  1112. }
  1113. static getAncestryAdjustedSpecialNameLowerCase(name, ancestryName) {
  1114. return `${name} (${ancestryName})`.toLowerCase();
  1115. }
  1116. static getHeritageAdjustedSpecialNameLowerCase(name, heritageName) {
  1117. return `${name} (${heritageName})`.toLowerCase();
  1118. }
  1119. static getChampionType(alignment) {
  1120. if (alignment == "LG") return "Paladin";
  1121. else if (alignment == "CG") return "Liberator";
  1122. else if (alignment == "NG") return "Redeemer";
  1123. else if (alignment == "LE") return "Tyrant";
  1124. else if (alignment == "CE") return "Antipaladin";
  1125. else if (alignment == "NE") return "Desecrator";
  1126. return "Unknown";
  1127. }
  1128. }
  1129. ;// CONCATENATED MODULE: ./src/app/CompendiumMatcher.js
  1130. /* eslint-disable no-await-in-loop */
  1131. class CompendiumMatcher {
  1132. constructor({ type, mappings = null, indexFields = ["name", "type", "system.slug"] } = {}) {
  1133. this.type = type;
  1134. this.indexFields = indexFields;
  1135. this.packs = {};
  1136. const packMappings = mappings !== null
  1137. ? mappings
  1138. : src_utils.setting("USE_CUSTOM_COMPENDIUM_MAPPINGS")
  1139. ? src_utils.setting("CUSTOM_COMPENDIUM_MAPPINGS")
  1140. : constants.CORE_COMPENDIUM_MAPPINGS;
  1141. packMappings[type].forEach((name) => {
  1142. const compendium = game.packs.get(name);
  1143. if (compendium) {
  1144. this.packs[name] = compendium;
  1145. }
  1146. });
  1147. this.indexes = {
  1148. };
  1149. }
  1150. async loadCompendiums() {
  1151. for (const [name, compendium] of Object.entries(this.packs)) {
  1152. this.indexes[name] = await compendium.getIndex({ fields: this.indexFields });
  1153. }
  1154. }
  1155. getFoundryFeatureName(pbName) {
  1156. const match = this.FEAT_RENAME_MAP(pbName).find((map) => map.pbName == pbName);
  1157. return match ?? { pbName, foundryName: pbName, details: undefined };
  1158. }
  1159. getNameMatch(pbName, foundryName) {
  1160. for (const [packName, index] of Object.entries(this.indexes)) {
  1161. const indexMatch = index.find((i) => i.name === foundryName)
  1162. ?? index.find((i) => i.name === pbName);
  1163. if (indexMatch) {
  1164. src_logger.debug(`Found name only compendium document for ${pbName} (${foundryName}) in ${packName} with id ${indexMatch._id}`);
  1165. return { i: indexMatch, pack: this.packs[packName] };
  1166. }
  1167. }
  1168. return undefined;
  1169. }
  1170. getSlugMatch(pbName, foundryName) {
  1171. for (const [packName, index] of Object.entries(this.indexes)) {
  1172. src_logger.debug(`Checking for compendium documents for ${pbName} (${foundryName}) in ${packName}`, {
  1173. pbName,
  1174. foundryName,
  1175. packName,
  1176. // index,
  1177. // foundrySlug: Seasoning.slug(foundryName),
  1178. // pbSlug: Seasoning.slug(pbName),
  1179. // foundryMatch: index.find((i) => (i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(foundryName)),
  1180. // pbMatch: index.find((i) => (i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(pbName)),
  1181. // pbSlugMatch: (null ?? Seasoning.slug("Phase Bolt (Psychic)")) === Seasoning.slug("Phase Bolt (Psychic)"),
  1182. });
  1183. const indexMatch = index.find((i) => (i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(foundryName))
  1184. ?? index.find((i) => (i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(pbName));
  1185. if (indexMatch) {
  1186. src_logger.debug(`Found slug based compendium document for ${pbName} (${foundryName}) in ${packName} with id ${indexMatch._id}`);
  1187. return { i: indexMatch, pack: this.packs[packName] };
  1188. }
  1189. }
  1190. return undefined;
  1191. }
  1192. getMatch(pbName, foundryName, forceName = false) {
  1193. if (forceName) {
  1194. const nameOnlyMatch = this.getNameMatch(pbName, foundryName);
  1195. if (nameOnlyMatch) return nameOnlyMatch;
  1196. }
  1197. const slugMatch = this.getSlugMatch(pbName, foundryName);
  1198. if (slugMatch) return slugMatch;
  1199. return undefined;
  1200. }
  1201. static checkForFilters(i, filters) {
  1202. for (const [key, value] of Object.entries(filters)) {
  1203. if (getProperty(i, key) !== value) {
  1204. return false;
  1205. }
  1206. }
  1207. return true;
  1208. }
  1209. getNameMatchWithFilter(pbName, foundryName, filters = {}) {
  1210. for (const [packName, index] of Object.entries(this.indexes)) {
  1211. src_logger.debug(`Checking for compendium documents for ${pbName} (${foundryName}) in ${packName}`, {
  1212. pbName,
  1213. foundryName,
  1214. filters,
  1215. packName,
  1216. // index,
  1217. });
  1218. const indexMatch = index.find((i) =>
  1219. ((i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(foundryName))
  1220. && CompendiumMatcher.checkForFilters(i, filters))
  1221. ?? index.find((i) =>
  1222. ((i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(pbName)
  1223. && CompendiumMatcher.checkForFilters(i, filters))
  1224. );
  1225. if (indexMatch) {
  1226. src_logger.debug(`Found compendium document for ${pbName} (${foundryName}) in ${packName} with id ${indexMatch._id}`);
  1227. return { i: indexMatch, pack: this.packs[packName] };
  1228. }
  1229. }
  1230. return undefined;
  1231. }
  1232. }
  1233. ;// CONCATENATED MODULE: ./src/app/CompendiumSelector.js
  1234. class CompendiumSelector extends FormApplication {
  1235. constructor() {
  1236. super();
  1237. this.lookups = src_utils.setting("CUSTOM_COMPENDIUM_MAPPINGS");
  1238. this.packs = game.packs
  1239. .filter((p) => p.metadata.type === "Item")
  1240. .map((p) => {
  1241. return { id: p.metadata.id, label: `${p.metadata.label} (${p.metadata.packageName})` };
  1242. });
  1243. this.currentType = null;
  1244. this.useCustomCompendiums = src_utils.setting("USE_CUSTOM_COMPENDIUM_MAPPINGS");
  1245. }
  1246. static get defaultOptions() {
  1247. return mergeObject(super.defaultOptions, {
  1248. id: "pathmuncher-compendium-selector",
  1249. template: `${constants.PATH}/templates/compendium-selector.hbs`,
  1250. width: 722,
  1251. height: 275,
  1252. title: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.CompendiumSelector.Title`),
  1253. resizable: true,
  1254. classes: ['pathmuncher-compendium-selector'],
  1255. });
  1256. }
  1257. getData() {
  1258. const lookups = [];
  1259. for (const key in this.lookups) {
  1260. lookups.push({
  1261. key,
  1262. label: game.i18n.localize(`${constants.FLAG_NAME}.CompendiumGroups.${key}`),
  1263. });
  1264. }
  1265. return {
  1266. lookups,
  1267. title: this.options.title,
  1268. sourceItems: [],
  1269. compendiumItems: [],
  1270. useCustomCompendiums: this.useCustomCompendiums,
  1271. };
  1272. }
  1273. async reset() {
  1274. const defaults = constants.GET_DEFAULT_SETTINGS();
  1275. this.lookups = defaults[constants.SETTINGS.CUSTOM_COMPENDIUM_MAPPINGS].default;
  1276. await src_utils.updateSetting("CUSTOM_COMPENDIUM_MAPPINGS", this.lookups);
  1277. this.currentType = null;
  1278. this.render(true);
  1279. }
  1280. async enableCustomCompendiums() {
  1281. this.useCustomCompendiums = !this.useCustomCompendiums;
  1282. await src_utils.updateSetting("USE_CUSTOM_COMPENDIUM_MAPPINGS", this.useCustomCompendiums);
  1283. }
  1284. filterList(event) {
  1285. const compendiumType = event.srcElement.value;
  1286. const sourceList = document.getElementById("sourceList");
  1287. const compendiumList = document.getElementById("compendiumList");
  1288. const sourceOptions = this.packs.filter((p) => !this.lookups[compendiumType].includes(p.id));
  1289. const compendiumOptions = this.packs.filter((p) => this.lookups[compendiumType].includes(p.id));
  1290. sourceList.innerHTML = "";
  1291. compendiumList.innerHTML = "";
  1292. sourceOptions.forEach((option) => {
  1293. const sourceListItem = document.createElement("option");
  1294. sourceListItem.value = option.id;
  1295. sourceListItem.appendChild(document.createTextNode(option.label));
  1296. sourceList.appendChild(sourceListItem);
  1297. });
  1298. compendiumOptions.forEach((option) => {
  1299. const compendiumListItem = document.createElement("option");
  1300. compendiumListItem.value = option.id;
  1301. compendiumListItem.appendChild(document.createTextNode(option.label));
  1302. compendiumList.appendChild(compendiumListItem);
  1303. });
  1304. this.currentType = compendiumType;
  1305. }
  1306. async updateCompendiums() {
  1307. const compendiumList = document.getElementById("compendiumList");
  1308. const compendiumOptions = Array.from(compendiumList.options);
  1309. const compendiumIds = compendiumOptions.map((option) => {
  1310. return option.value;
  1311. });
  1312. this.lookups[this.currentType] = compendiumIds;
  1313. src_utils.updateSetting("CUSTOM_COMPENDIUM_MAPPINGS", this.lookups);
  1314. }
  1315. async addCompendium() {
  1316. const sourceList = document.getElementById("sourceList");
  1317. const compendiumList = document.getElementById("compendiumList");
  1318. const selectedOptions = Array.from(sourceList.selectedOptions);
  1319. selectedOptions.forEach((option) => {
  1320. compendiumList.appendChild(option);
  1321. });
  1322. await this.updateCompendiums();
  1323. }
  1324. async removeCompendium() {
  1325. const sourceList = document.getElementById("sourceList");
  1326. const compendiumList = document.getElementById("compendiumList");
  1327. const selectedOptions = Array.from(compendiumList.selectedOptions);
  1328. selectedOptions.forEach((option) => {
  1329. sourceList.appendChild(option);
  1330. });
  1331. await this.updateCompendiums();
  1332. }
  1333. async moveUp() {
  1334. const compendiumList = document.getElementById("compendiumList");
  1335. const selectedOption = compendiumList.selectedOptions[0];
  1336. if (selectedOption && selectedOption.previousElementSibling) {
  1337. compendiumList.insertBefore(selectedOption, selectedOption.previousElementSibling);
  1338. }
  1339. await this.updateCompendiums();
  1340. }
  1341. async moveDown() {
  1342. const compendiumList = document.getElementById("compendiumList");
  1343. const selectedOption = compendiumList.selectedOptions[0];
  1344. if (selectedOption && selectedOption.nextElementSibling) {
  1345. compendiumList.insertBefore(selectedOption.nextElementSibling, selectedOption);
  1346. }
  1347. await this.updateCompendiums();
  1348. }
  1349. activateListeners(html) {
  1350. super.activateListeners(html);
  1351. document.getElementById("addButton").addEventListener("click", this.addCompendium.bind(this));
  1352. document.getElementById("removeButton").addEventListener("click", this.removeCompendium.bind(this));
  1353. document.getElementById("upButton").addEventListener("click", this.moveUp.bind(this));
  1354. document.getElementById("downButton").addEventListener("click", this.moveDown.bind(this));
  1355. document.getElementById("compSelector").addEventListener("change", this.filterList.bind(this));
  1356. document.getElementById("resetButton").addEventListener("click", this.reset.bind(this));
  1357. document.getElementById("enableCustomCompendiums").addEventListener("change", this.enableCustomCompendiums.bind(this));
  1358. }
  1359. }
  1360. ;// CONCATENATED MODULE: ./src/app/Pathmuncher.js
  1361. /* eslint-disable no-await-in-loop */
  1362. /* eslint-disable no-continue */
  1363. class Pathmuncher {
  1364. FEAT_RENAME_MAP(name) {
  1365. const dynamicItems = [
  1366. { pbName: "Shining Oath", foundryName: `Shining Oath (${Seasoning.getChampionType(this.source.alignment)})` },
  1367. { pbName: "Counterspell", foundryName: `Counterspell (${src_utils.capitalize(this.getClassSpellCastingType() ?? "")})` },
  1368. { pbName: "Counterspell", foundryName: `Counterspell (${src_utils.capitalize(this.getClassSpellCastingType(true) ?? "")})` },
  1369. { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${this.source.class})` },
  1370. { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${this.source.dualClass})` },
  1371. { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${src_utils.capitalize(this.getClassSpellCastingType() ?? "")} Caster)` },
  1372. { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${src_utils.capitalize(this.getClassSpellCastingType(true) ?? "")} Caster)` },
  1373. ];
  1374. return Seasoning.FEAT_RENAME_MAP(name).concat(dynamicItems);
  1375. }
  1376. getFoundryFeatureName(pbName, isSpecial = false) {
  1377. if (isSpecial) {
  1378. const specialMatch = specialOnlyNameLookup(pbName);
  1379. if (specialMatch) return specialMatch;
  1380. }
  1381. const match = this.FEAT_RENAME_MAP(pbName).find((map) => map.pbName == pbName);
  1382. return match ?? { pbName, foundryName: pbName, details: undefined };
  1383. }
  1384. constructor(actor, { addFeats = true, addEquipment = true, addSpells = true, adjustBlendedSlots = true,
  1385. addMoney = true, addLores = true, addWeapons = true, addArmor = true, addTreasure = true, addDeity = true,
  1386. addName = true, addClass = true, addBackground = true, addHeritage = true, addAncestry = true,
  1387. statusCallback = null } = {}
  1388. ) {
  1389. this.devMode = game.modules.get("pathmuncher").version === "999.0.0";
  1390. this.actor = actor;
  1391. // note not all these options do anything yet!
  1392. this.options = {
  1393. addTreasure,
  1394. addMoney,
  1395. addFeats,
  1396. addSpells,
  1397. adjustBlendedSlots,
  1398. addEquipment,
  1399. addLores,
  1400. addWeapons,
  1401. addArmor,
  1402. addDeity,
  1403. addName,
  1404. addClass,
  1405. addBackground,
  1406. addHeritage,
  1407. addAncestry,
  1408. };
  1409. this.source = null;
  1410. this.parsed = {
  1411. specials: [],
  1412. feats: [],
  1413. equipment: [],
  1414. armor: [],
  1415. weapons: [],
  1416. };
  1417. this.usedLocations = new Set();
  1418. this.usedLocationsAlternateRules = new Set();
  1419. this.autoAddedFeatureIds = new Set();
  1420. this.autoAddedFeatureItems = {};
  1421. this.promptRules = {};
  1422. this.allFeatureRules = {};
  1423. this.autoAddedFeatureRules = {};
  1424. this.grantItemLookUp = {};
  1425. this.autoFeats = [];
  1426. this.keyAbility = null;
  1427. this.boosts = {
  1428. custom: false,
  1429. class: {},
  1430. background: {},
  1431. ancestry: {},
  1432. };
  1433. this.size = "med";
  1434. this.result = {
  1435. character: {
  1436. _id: this.actor.id,
  1437. prototypeToken: {},
  1438. },
  1439. class: [],
  1440. deity: [],
  1441. heritage: [],
  1442. ancestry: [],
  1443. background: [],
  1444. casters: [],
  1445. spells: [],
  1446. feats: [],
  1447. weapons: [],
  1448. armor: [],
  1449. equipment: [],
  1450. lores: [],
  1451. money: [],
  1452. treasure: [],
  1453. adventurersPack: {
  1454. item: null,
  1455. contents: [
  1456. { slug: "bedroll", qty: 1 },
  1457. { slug: "chalk", qty: 10 },
  1458. { slug: "flint-and-steel", qty: 1 },
  1459. { slug: "rope", qty: 1 },
  1460. { slug: "rations", qty: 14 },
  1461. { slug: "torch", qty: 5 },
  1462. { slug: "waterskin", qty: 1 },
  1463. ],
  1464. },
  1465. focusPool: 0,
  1466. };
  1467. this.check = {};
  1468. this.bad = [];
  1469. this.statusCallback = statusCallback;
  1470. this.compendiumMatchers = {};
  1471. const compendiumMappings = src_utils.setting("USE_CUSTOM_COMPENDIUM_MAPPINGS")
  1472. ? src_utils.setting("CUSTOM_COMPENDIUM_MAPPINGS")
  1473. : constants.CORE_COMPENDIUM_MAPPINGS;
  1474. for (const type of Object.keys(compendiumMappings)) {
  1475. this.compendiumMatchers[type] = new CompendiumMatcher({ type });
  1476. }
  1477. }
  1478. async #loadCompendiumMatchers() {
  1479. for (const matcher of Object.values(this.compendiumMatchers)) {
  1480. await matcher.loadCompendiums();
  1481. }
  1482. }
  1483. #statusUpdate(total, count, type, prefixLabel) {
  1484. if (this.statusCallback) this.statusCallback(total, count, type, prefixLabel);
  1485. }
  1486. async fetchPathbuilder(pathbuilderId) {
  1487. if (!pathbuilderId) {
  1488. const flags = src_utils.getFlags(this.actor);
  1489. pathbuilderId = flags?.pathbuilderId;
  1490. }
  1491. if (pathbuilderId) {
  1492. const jsonData = await foundry.utils.fetchJsonWithTimeout(
  1493. `https://www.pathbuilder2e.com/json.php?id=${pathbuilderId}`
  1494. );
  1495. if (jsonData.success) {
  1496. this.source = jsonData.build;
  1497. } else {
  1498. ui.notifications.warn(
  1499. game.i18n.format(`${constants.FLAG_NAME}.Dialogs.Pathmuncher.FetchFailed`, { pathbuilderId })
  1500. );
  1501. }
  1502. } else {
  1503. ui.notifications.error(game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.Pathmuncher.NoId`));
  1504. }
  1505. }
  1506. #generateFoundryFeatLocation(document, feature) {
  1507. if (feature.type && feature.level) {
  1508. // const freeArchetypeVariant = game.settings.get("pf2e", "freeArchetypeVariant");
  1509. const location = Seasoning.getFoundryFeatLocation(feature.type, feature.level);
  1510. if (location && !this.usedLocations.has(location)) {
  1511. document.system.location = location;
  1512. this.usedLocations.add(location);
  1513. } else if (location && this.usedLocations.has(location)) {
  1514. src_logger.debug("Variant feat location", { ancestryParagonVariant: src_utils.allowAncestryParagon(), location, feature });
  1515. // eslint-disable-next-line max-depth
  1516. if (src_utils.allowAncestryParagon() && feature.type === "Ancestry Feat") {
  1517. document.system.location = "ancestry-bonus";
  1518. this.usedLocationsAlternateRules.add(location);
  1519. } else if (src_utils.allowDualClasses() && feature.type === "Class Feat") {
  1520. document.system.location = `dualclass-${feature.level}`;
  1521. this.usedLocationsAlternateRules.add(location);
  1522. }
  1523. }
  1524. }
  1525. }
  1526. #processSpecialData(name) {
  1527. if (name.includes("Domain: ")) {
  1528. const domainName = name.split(" ")[1];
  1529. this.parsed.feats.push({ name: "Deity's Domain", extra: domainName });
  1530. return true;
  1531. } else {
  1532. return false;
  1533. }
  1534. }
  1535. #getContainerData(key) {
  1536. return {
  1537. id: key,
  1538. containerName: this.source.equipmentContainers[key].containerName,
  1539. bagOfHolding: this.source.equipmentContainers[key].bagOfHolding,
  1540. backpack: this.source.equipmentContainers[key].backpack,
  1541. };
  1542. }
  1543. #nameMapSourceEquipment(e) {
  1544. const name = Seasoning.getFoundryEquipmentName(e[0]);
  1545. const containerKey = Object.keys(this.source.equipmentContainers)
  1546. .find((key) => this.source.equipmentContainers[key].containerName === name);
  1547. const container = containerKey ? this.#getContainerData(containerKey) : null;
  1548. const foundryId = foundry.utils.randomID();
  1549. if (container) {
  1550. this.source.equipmentContainers[containerKey].foundryId = foundryId;
  1551. }
  1552. const item = {
  1553. foundryName: name,
  1554. pbName: e[0],
  1555. originalName: e[0],
  1556. qty: e[1],
  1557. added: false,
  1558. addedId: null,
  1559. addedAutoId: null,
  1560. inContainer: e[2] !== "Invested" ? e[2] : null,
  1561. container,
  1562. foundryId,
  1563. invested: e[2] === "Invested",
  1564. sourceType: "equipment",
  1565. };
  1566. this.parsed.equipment.push(item);
  1567. }
  1568. #nameMapSourceEquipmentAddHandwraps(e) {
  1569. const name = Seasoning.getFoundryEquipmentName(e[0]);
  1570. const potencyMatch = e[0].match(/\(\+(\d)[\s)]/i);
  1571. const potency = potencyMatch ? parseInt(potencyMatch[1]) : 0;
  1572. const strikingMatch = e[0].match(/\d( \w*)? (Striking)/i);
  1573. const striking = strikingMatch
  1574. ? Seasoning.slugD(`${(strikingMatch[1] ?? "").trim()}${(strikingMatch[2] ?? "").trim()}`) // `${(strikingMatch[2] ?? "").toLowerCase().trim()}${(strikingMatch[1] ?? "").trim()}`.trim()
  1575. : "";
  1576. const mockE = {
  1577. name: e[0],
  1578. qty: 1,
  1579. prof: "unarmed",
  1580. pot: Number.isInteger(potency) ? potency : 0,
  1581. str: striking,
  1582. mat: null,
  1583. display: e[0],
  1584. runes: [],
  1585. damageType: "B",
  1586. increasedDice: false
  1587. };
  1588. const weapon = mergeObject({
  1589. foundryName: name,
  1590. pbName: mockE.name,
  1591. originalName: mockE.name,
  1592. added: false,
  1593. addedId: null,
  1594. addedAutoId: null,
  1595. sourceType: "weapons",
  1596. }, mockE);
  1597. this.parsed.weapons.push(weapon);
  1598. }
  1599. #nameMap() {
  1600. let iRank = 0;
  1601. let featRank = 0;
  1602. src_logger.debug("Starting Equipment Rename");
  1603. this.source.equipment
  1604. .filter((e) => e[0] && e[0] !== "undefined")
  1605. .forEach((e) => {
  1606. if (e[0].startsWith("Handwraps of Mighty Blows")) {
  1607. this.#nameMapSourceEquipmentAddHandwraps(e);
  1608. } else {
  1609. this.#nameMapSourceEquipment(e);
  1610. }
  1611. });
  1612. this.source.armor
  1613. .filter((e) => e && e !== "undefined")
  1614. .forEach((e) => {
  1615. const name = Seasoning.getFoundryEquipmentName(e.name);
  1616. const item = mergeObject({
  1617. foundryName: name,
  1618. pbName: e.name,
  1619. originalName: e.name,
  1620. added: false,
  1621. addedId: null,
  1622. addedAutoId: null,
  1623. sourceType: "armor",
  1624. }, e);
  1625. this.parsed.armor.push(item);
  1626. // work around for now
  1627. if (e.name.startsWith("Inventor ")) {
  1628. this.parsed.feats.push({
  1629. name,
  1630. extra: "",
  1631. added: false,
  1632. addedId: null,
  1633. addedAutoId: null,
  1634. type: "Awarded Feat",
  1635. level: 1,
  1636. originalName: e.name,
  1637. rank: 0,
  1638. sourceType: "armor",
  1639. featChoiceRef: null,
  1640. hasChildren: null,
  1641. isChild: null,
  1642. isStandard: null,
  1643. parentFeatChoiceRef: null,
  1644. });
  1645. featRank++;
  1646. }
  1647. });
  1648. this.source.weapons
  1649. .filter((e) => e && e !== "undefined")
  1650. .forEach((e) => {
  1651. const name = Seasoning.getFoundryEquipmentName(e.name);
  1652. const item = mergeObject({
  1653. foundryName: name,
  1654. pbName: e.name,
  1655. originalName: e.name,
  1656. added: false,
  1657. addedId: null,
  1658. addedAutoId: null,
  1659. sourceType: "weapons",
  1660. }, e);
  1661. this.parsed.weapons.push(item);
  1662. });
  1663. src_logger.debug("Finished Equipment Rename");
  1664. src_logger.debug("Starting Special Rename");
  1665. [].concat(this.source.specials, SPECIAL_NAME_ADDITIONS(this.source.specials))
  1666. .filter((special) =>
  1667. special
  1668. && special !== "undefined"
  1669. && special !== "Not Selected"
  1670. && special !== this.source.heritage
  1671. )
  1672. .forEach((special) => {
  1673. const match = this.getFoundryFeatureName(special); // , true);
  1674. if (!this.#processSpecialData(match.foundryName) && !Seasoning.IGNORED_SPECIALS().includes(match.foundryName)) {
  1675. this.parsed.specials.push({ name: match.foundryName, foundryName: match.foundryName, foundryValue: match.foundryValue, originalName: special, added: false, addedId: null, addedAutoId: null, rank: iRank, sourceType: "specials" });
  1676. iRank++;
  1677. }
  1678. });
  1679. src_logger.debug("Finished Special Rename");
  1680. src_logger.debug("Starting Feat Rename");
  1681. this.source.feats
  1682. .filter((feat) =>
  1683. feat[0]
  1684. && feat[0] !== "undefined"
  1685. && feat[0] !== "Not Selected"
  1686. // && feat[0] !== this.source.heritage
  1687. )
  1688. .forEach((feat) => {
  1689. const name = this.getFoundryFeatureName(feat[0]).foundryName;
  1690. const data = {
  1691. name,
  1692. extra: feat[1],
  1693. added: feat[0] === this.source.heritage,
  1694. addedId: null,
  1695. addedAutoId: null,
  1696. type: feat[2],
  1697. level: feat[3],
  1698. originalName: feat[0],
  1699. rank: featRank,
  1700. sourceType: "feats",
  1701. };
  1702. if (feat.length >= 7) {
  1703. data.featChoiceRef = feat[4];
  1704. data.hasChildren = feat[5] === "parentChoice";
  1705. data.isChild = feat[5] === "childChoice";
  1706. data.isStandard = feat[5] === "standardChoice";
  1707. data.parentFeatChoiceRef = feat[6];
  1708. const parentFeatMatch = this.source.feats.find((f) =>
  1709. feat[5] === "childChoice"
  1710. && (data.featChoiceRef.toLowerCase().startsWith(f[0].toLowerCase())
  1711. || (data.parentFeatChoiceRef
  1712. && data.featChoiceRef.replace(data.parentFeatChoiceRef, "").trim().toLowerCase().startsWith(f[0].toLowerCase()))
  1713. )
  1714. );
  1715. data.nameHint = parentFeatMatch?.[0];
  1716. } else {
  1717. // probably an awarded feat
  1718. data.featChoiceRef = null;
  1719. data.hasChildren = null;
  1720. data.isChild = null;
  1721. data.isStandard = null;
  1722. data.parentFeatChoiceRef = null;
  1723. }
  1724. this.parsed.feats.push(data);
  1725. featRank++;
  1726. });
  1727. src_logger.debug("Finished Feat Rename");
  1728. src_logger.debug("Name remapping results", {
  1729. parsed: this.parsed,
  1730. });
  1731. }
  1732. #fixUps() {
  1733. if (this.source.ancestry === "Dwarf" && !this.parsed.feats.some((f) => f.name === "Clan Pistol")) {
  1734. const clanDagger = {
  1735. name: "Clan Dagger",
  1736. originalName: "Clan Dagger",
  1737. added: false,
  1738. addedId: null,
  1739. addedAutoId: null,
  1740. isChoice: true,
  1741. rank: 0,
  1742. sourceType: "specials",
  1743. };
  1744. this.parsed.specials.push(clanDagger);
  1745. }
  1746. const match = this.source.background.match(/(Magical Experiment) \((.*)\)$/);
  1747. if (match) {
  1748. this.parsed.specials.push({
  1749. name: match[2],
  1750. originalName: `${this.source.background}`,
  1751. added: false,
  1752. addedId: null,
  1753. addedAutoId: null,
  1754. isChoice: true,
  1755. rank: 0,
  1756. sourceType: "specials",
  1757. });
  1758. this.source.background = match[1];
  1759. }
  1760. }
  1761. async #prepare() {
  1762. await this.#loadCompendiumMatchers();
  1763. this.#nameMap();
  1764. this.#fixUps();
  1765. }
  1766. async #processSenses() {
  1767. const senses = [];
  1768. this.source.specials.forEach((special) => {
  1769. if (special === "Low-Light Vision") {
  1770. senses.push({ type: "lowLightVision" });
  1771. } else if (special === "Darkvision") {
  1772. senses.push({ type: "darkvision" });
  1773. } else if (special === "Scent") {
  1774. senses.push({ type: "scent" });
  1775. }
  1776. });
  1777. setProperty(this.result.character, "system.traits.senses", senses);
  1778. }
  1779. // eslint-disable-next-line no-unused-vars
  1780. async #addDualClass(klass) {
  1781. if (!src_utils.allowDualClasses()) {
  1782. if (this.source.dualClass && this.source.dualClass !== "") {
  1783. src_logger.warn(`Imported character is dual class. Pathmuncher does not support dual class characters, please check the system macros`, {
  1784. class: this.source.class,
  1785. dualClass: this.source.dualClass,
  1786. });
  1787. ui.notifications.warn(`Imported character is dual class. Pathmuncher does not support dual class characters, please check the system macros`);
  1788. }
  1789. return;
  1790. }
  1791. if (!this.source.dualClass || this.source.dualClass === "") {
  1792. src_logger.warn(`Imported character not dual class but system is configured for dual class`, {
  1793. class: this.source.class,
  1794. });
  1795. ui.notifications.warn(`Imported character not dual class but system is configured for dual class`);
  1796. return;
  1797. }
  1798. src_logger.info("Not processing dual class");
  1799. // // find the dual class
  1800. // const foundryName = this.getFoundryFeatureName(this.source.dualClass).foundryName;
  1801. // const indexMatch = this.compendiumMatchers["classes"].getMatch(this.source.dualClass, foundryName);
  1802. // if (!indexMatch) return;
  1803. // const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  1804. // const dualClass = doc.toObject();
  1805. // logger.debug(`Dual Class ${dualClass.name} found, squashing things together.`);
  1806. // klass.name = `${klass.name} - ${dualClass.name}`;
  1807. // const ruleEntry = {
  1808. // domain: "all",
  1809. // key: "RollOption",
  1810. // option: `class:${dualClass.system.slug}`,
  1811. // };
  1812. // // Attacks
  1813. // ["advanced", "martial", "simple", "unarmed"].forEach((key) => {
  1814. // if (dualClass.system.attacks[key] > klass.system.attacks[key]) {
  1815. // klass.system.attacks[key] = dualClass.system.attacks[key];
  1816. // }
  1817. // });
  1818. // if (klass.system.attacks.martial <= dualClass.system.attacks.other.rank) {
  1819. // if (dualClass.system.attacks.other.rank === klass.system.attacks.other.rank) {
  1820. // let mashed = `${klass.system.attacks.other.name}, ${dualClass.system.attacks.other.name}`;
  1821. // mashed = mashed.replace("and ", "");
  1822. // klass.system.attacks.other.name = [...new Set(mashed.split(","))].join(",");
  1823. // }
  1824. // if (dualClass.system.attacks.other.rank > klass.system.attacks.other.rank) {
  1825. // klass.system.attacks.other.name = dualClass.system.attacks.other.name;
  1826. // klass.system.attacks.other.rank = dualClass.system.attacks.other.rank;
  1827. // }
  1828. // }
  1829. // if (
  1830. // klass.system.attacks.martial >= dualClass.system.attacks.other.rank
  1831. // && klass.system.attacks.martial >= klass.system.attacks.other.rank
  1832. // ) {
  1833. // klass.system.attacks.other.rank = 0;
  1834. // klass.system.attacks.other.name = "";
  1835. // }
  1836. // // Class DC
  1837. // if (dualClass.system.classDC > klass.system.classDC) {
  1838. // klass.system.classDC = dualClass.system.classDC;
  1839. // }
  1840. // // Defenses
  1841. // ["heavy", "light", "medium", "unarmored"].forEach((key) => {
  1842. // if (dualClass.system.defenses[key] > klass.system.defenses[key]) {
  1843. // klass.system.defenses[key] = dualClass.system.defenses[key];
  1844. // }
  1845. // });
  1846. // // Description
  1847. // klass.system.description.value = `${klass.system.description.value} ${dualClass.system.description.value}`;
  1848. // // HP
  1849. // if (dualClass.system.hp > klass.system.hp) {
  1850. // klass.system.hp = dualClass.system.hp;
  1851. // }
  1852. // // Items
  1853. // Object.entries(dualClass.system.items).forEach((i) => {
  1854. // if (Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level > i[1].level)) {
  1855. // Object.values(klass.system.items).find((x) => x.uuid === i[1].uuid).level = i[1].level;
  1856. // } else if (!Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level <= i[1].level)) {
  1857. // klass.system.items[i[0]] = i[1];
  1858. // }
  1859. // });
  1860. // // Key Ability
  1861. // dualClass.system.keyAbility.value.forEach((v) => {
  1862. // if (!klass.system.keyAbility.value.includes(v)) {
  1863. // klass.system.keyAbility.value.push(v);
  1864. // }
  1865. // });
  1866. // // Perception
  1867. // if (dualClass.system.perception > klass.system.perception) klass.system.perception = dualClass.system.perception;
  1868. // // Rules
  1869. // klass.system.rules.push(ruleEntry);
  1870. // dualClass.system.rules.forEach((r) => {
  1871. // if (!klass.system.rules.includes(r)) {
  1872. // klass.system.rules.push(r);
  1873. // }
  1874. // });
  1875. // klass.system.rules.forEach((r, i) => {
  1876. // if (r.path !== undefined) {
  1877. // const check = r.path.split(".");
  1878. // if (
  1879. // check.includes("data")
  1880. // && check.includes("martial")
  1881. // && check.includes("rank")
  1882. // && klass.system.attacks.martial >= r.value
  1883. // ) {
  1884. // klass.system.rules.splice(i, 1);
  1885. // }
  1886. // }
  1887. // });
  1888. // // Saving Throws
  1889. // ["fortitude", "reflex", "will"].forEach((key) => {
  1890. // if (dualClass.system.savingThrows[key] > klass.system.savingThrows[key]) {
  1891. // klass.system.savingThrows[key] = dualClass.system.savingThrows[key];
  1892. // }
  1893. // });
  1894. // // Skill Feat Levels
  1895. // dualClass.system.skillFeatLevels.value.forEach((v) => {
  1896. // klass.system.skillFeatLevels.value.push(v);
  1897. // });
  1898. // klass.system.skillFeatLevels.value = [...new Set(klass.system.skillFeatLevels.value)].sort((a, b) => {
  1899. // return a - b;
  1900. // });
  1901. // // Skill Increase Levels
  1902. // dualClass.system.skillIncreaseLevels.value.forEach((v) => {
  1903. // klass.system.skillIncreaseLevels.value.push(v);
  1904. // });
  1905. // klass.system.skillIncreaseLevels.value = [...new Set(klass.system.skillIncreaseLevels.value)].sort((a, b) => {
  1906. // return a - b;
  1907. // });
  1908. // // Trained Skills
  1909. // if (dualClass.system.trainedSkills.additional > klass.system.trainedSkills.additional) {
  1910. // klass.system.trainedSkills.additional = dualClass.system.trainedSkills.additional;
  1911. // }
  1912. // dualClass.system.trainedSkills.value.forEach((v) => {
  1913. // if (!klass.system.trainedSkills.value.includes(v)) {
  1914. // klass.system.trainedSkills.value.push(v);
  1915. // }
  1916. // });
  1917. // this.result.dualClass = dualClass;
  1918. }
  1919. // eslint-disable-next-line class-methods-use-this
  1920. async #processGenericCompendiumLookup(type, name, target) {
  1921. src_logger.debug(`Checking for compendium documents for ${name} (${target}) in compendiums for ${type}`);
  1922. const foundryName = this.getFoundryFeatureName(name).foundryName;
  1923. const indexMatch = this.compendiumMatchers[type].getMatch(name, foundryName);
  1924. if (indexMatch) {
  1925. const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  1926. const itemData = doc.toObject();
  1927. if (name.includes("(")) {
  1928. const extra = name.split(")")[0].split("(").pop();
  1929. this.parsed.specials.push({ name: doc.name, originalName: name, added: true, extra, rank: 99 });
  1930. }
  1931. if (target === "class") {
  1932. itemData.system.keyAbility.selected = this.keyAbility;
  1933. await this.#addDualClass(itemData);
  1934. }
  1935. itemData._id = foundry.utils.randomID();
  1936. // this.#generateGrantItemData(itemData);
  1937. this.result[target].push(itemData);
  1938. await this.#addGrantedItems(itemData, { applyFeatLocation: target !== "class" });
  1939. return true;
  1940. } else {
  1941. this.bad.push({ pbName: name, type: target, details: { name } });
  1942. return false;
  1943. }
  1944. }
  1945. // for grants, e.g. ont he champion "Deity and Cause" where there are choices.
  1946. // how do we determine and match these? should we?
  1947. // "pf2e": {
  1948. // "itemGrants": {
  1949. // "adanye": {
  1950. // "id": "4GHcp3iaREfj2ZgN",
  1951. // "onDelete": "detach"
  1952. // },
  1953. // "paladin": {
  1954. // "id": "HGWkTEatliHgDaEu",
  1955. // "onDelete": "detach"
  1956. // }
  1957. // }
  1958. // }
  1959. // "Paladin" (granted by deity and casue)
  1960. // "pf2e": {
  1961. // "grantedBy": {
  1962. // "id": "xnrkrJa2YE1UOAVy",
  1963. // "onDelete": "cascade"
  1964. // },
  1965. // "itemGrants": {
  1966. // "retributiveStrike": {
  1967. // "id": "WVHbj9LljCTovdsv",
  1968. // "onDelete": "detach"
  1969. // }
  1970. // }
  1971. // }
  1972. // retributive strike
  1973. // "pf2e": {
  1974. // "grantedBy": {
  1975. // "id": "HGWkTEatliHgDaEu",
  1976. // "onDelete": "cascade"
  1977. // }
  1978. #slugNameMatch(f, slug) {
  1979. return slug === Seasoning.slug(f.name)
  1980. || slug === Seasoning.slug(f.foundryValue)
  1981. || slug === Seasoning.slug(Seasoning.getClassAdjustedSpecialNameLowerCase(f.name, this.source.class))
  1982. || slug === Seasoning.slug(Seasoning.getAncestryAdjustedSpecialNameLowerCase(f.name, this.source.ancestry))
  1983. || slug === Seasoning.slug(Seasoning.getHeritageAdjustedSpecialNameLowerCase(f.name, this.source.heritage))
  1984. || slug === Seasoning.slug(f.originalName)
  1985. || slug === Seasoning.slug(Seasoning.getClassAdjustedSpecialNameLowerCase(f.originalName, this.source.class))
  1986. || slug
  1987. === Seasoning.slug(Seasoning.getAncestryAdjustedSpecialNameLowerCase(f.originalName, this.source.ancestry))
  1988. || slug
  1989. === Seasoning.slug(Seasoning.getHeritageAdjustedSpecialNameLowerCase(f.originalName, this.source.heritage))
  1990. || (src_utils.allowDualClasses()
  1991. && (slug
  1992. === Seasoning.slug(Seasoning.getDualClassAdjustedSpecialNameLowerCase(f.name, this.source.dualClass))
  1993. || slug
  1994. === Seasoning.slug(
  1995. Seasoning.getDualClassAdjustedSpecialNameLowerCase(f.originalName, this.source.dualClass)
  1996. )));
  1997. }
  1998. #parsedFeatureMatch(type, document, slug, { ignoreAdded, isChoiceMatch = false, featType = null } = {}) {
  1999. if (type === "feats" && document) {
  2000. const hintMatch = this.parsed[type].find((f) =>
  2001. (!ignoreAdded || (ignoreAdded && !f.added))
  2002. && f.isChild
  2003. && f.nameHint
  2004. && Seasoning.slug(document.name) === Seasoning.slug(f.nameHint)
  2005. && this.#slugNameMatch(f, slug)
  2006. );
  2007. if (hintMatch) {
  2008. hintMatch.rank = -10;
  2009. return hintMatch;
  2010. }
  2011. }
  2012. // console.warn(`Trying to find ${slug} in ${type}, ignoreAdded? ${ignoreAdded}`, this.parsed[type]);
  2013. const parsedMatch = this.parsed[type].find((f) =>
  2014. (!ignoreAdded || (ignoreAdded && !f.added))
  2015. && (
  2016. featType === null
  2017. || f.type === featType
  2018. )
  2019. && !f.isChoice
  2020. && this.#slugNameMatch(f, slug)
  2021. );
  2022. if (parsedMatch || !document) return parsedMatch;
  2023. const extraMatch = this.parsed[type].find((f) =>
  2024. // (!ignoreAdded || (ignoreAdded && !f.added))
  2025. f.extra
  2026. && f.added
  2027. && !f.isChoice
  2028. && Seasoning.slug(f.name) === (document.system.slug ?? Seasoning.slug(document.name))
  2029. && Seasoning.slug(f.extra) === slug
  2030. );
  2031. if (extraMatch) return extraMatch;
  2032. if (isChoiceMatch) {
  2033. // console.warn("Specials check", {
  2034. // document,
  2035. // type,
  2036. // slug,
  2037. // });
  2038. const choiceMatch = this.parsed[type].find((f) => f.isChoice && !f.added && Seasoning.slug(f.name) === slug);
  2039. return choiceMatch;
  2040. }
  2041. return undefined;
  2042. }
  2043. #generatedResultMatch(type, slug) {
  2044. const featMatch = this.result[type].find((f) => slug === f.system.slug);
  2045. return featMatch;
  2046. }
  2047. #findAllFeatureMatch(document, slug, { ignoreAdded, isChoiceMatch = false, featType = null } = {}) {
  2048. // console.warn("Finding all feature matches", { document, slug, ignoreAdded, isChoiceMatch, featType });
  2049. const featMatch = this.#parsedFeatureMatch("feats", document, slug, { ignoreAdded, featType });
  2050. if (featMatch) return featMatch;
  2051. const specialMatch = this.#parsedFeatureMatch("specials", document, slug, { ignoreAdded, isChoiceMatch });
  2052. if (specialMatch) return specialMatch;
  2053. const deityMatch = this.#generatedResultMatch("deity", slug);
  2054. return deityMatch;
  2055. // const classMatch = this.#generatedResultMatch("class", slug);
  2056. // return classMatch;
  2057. // const equipmentMatch = this.#generatedResultMatch("equipment", slug);
  2058. // return equipmentMatch;
  2059. }
  2060. #createGrantedItem(document, parent, { itemGrantName = null, originType = null, applyFeatLocation = false } = {}) {
  2061. src_logger.debug(`Adding granted item flags to ${document.name} (parent ${parent.name}) with originType "${originType}", and will applyFeatLocation? ${applyFeatLocation}`);
  2062. if (itemGrantName) {
  2063. const camelCase = Seasoning.slugD(itemGrantName ?? document.system.slug ?? document.name);
  2064. setProperty(parent, `flags.pf2e.itemGrants.${camelCase}`, { id: document._id, onDelete: "detach" });
  2065. setProperty(document, "flags.pf2e.grantedBy", { id: parent._id, onDelete: "cascade" });
  2066. }
  2067. this.autoFeats.push(document);
  2068. this.result.feats.push(document);
  2069. const matchOptions = { ignoreAdded: true, featType: originType };
  2070. const featureMatch
  2071. = this.#findAllFeatureMatch(document, document.system.slug ?? Seasoning.slug(document.name), matchOptions)
  2072. ?? (document.name.includes("(")
  2073. ? this.#findAllFeatureMatch(document, Seasoning.slug(document.name.split("(")[0].trim()), matchOptions)
  2074. : undefined);
  2075. if (featureMatch) {
  2076. src_logger.debug(`Found feature match for ${document.name}`, { featureMatch });
  2077. const existingMatch = false;
  2078. // featureMatch.sourceType
  2079. // ? this.parsed[featureMatch.sourceType].some((f) => f.addedId === document._id)
  2080. // : false;
  2081. if (this.devMode && existingMatch) {
  2082. src_logger.warn(`create Granted Item Existing match for ${document.name}`, { featureMatch, existingMatch, document });
  2083. }
  2084. // console.warn(`Match for ${document.name} createGrantedItem`, { featureMatch, existingMatch, document });
  2085. if (hasProperty(featureMatch, "added") && !existingMatch) {
  2086. featureMatch.added = true;
  2087. featureMatch.addedId = document._id;
  2088. if (applyFeatLocation) this.#generateFoundryFeatLocation(document, featureMatch);
  2089. }
  2090. return;
  2091. }
  2092. if (document.type !== "action")
  2093. src_logger.warn(
  2094. `Unable to find parsed feature match for granted feature ${document.name}. This might not be an issue, but might indicate feature duplication.`,
  2095. { document, parent }
  2096. );
  2097. }
  2098. static #getLowestChoiceRank(choices) {
  2099. return choices.reduce((p, c) => {
  2100. return p.rank > c.rank ? c : p;
  2101. });
  2102. }
  2103. async #featureChoiceMatch(document, choices, ignoreAdded, adjustName, choiceHint = null) {
  2104. const matches = [];
  2105. for (const choice of choices) {
  2106. const doc = adjustName ? game.i18n.localize(choice.label) : await fromUuid(choice.value);
  2107. if (!doc) continue;
  2108. const slug = adjustName
  2109. ? Seasoning.slug(doc)
  2110. : doc.system.slug === null
  2111. ? Seasoning.slug(doc.name)
  2112. : doc.system.slug;
  2113. const featMatch = this.#findAllFeatureMatch(document, slug, { ignoreAdded, isChoiceMatch: false });
  2114. if (featMatch) {
  2115. matches.push({
  2116. slug,
  2117. rank: featMatch.rank,
  2118. choice,
  2119. featMatch,
  2120. });
  2121. }
  2122. }
  2123. if (matches.length > 0) {
  2124. if (choiceHint) {
  2125. const hintMatch = matches.find((m) => m.slug === Seasoning.slug(choiceHint));
  2126. if (hintMatch) return hintMatch;
  2127. }
  2128. if (this.devMode) src_logger.warn(`MATCHES`, matches);
  2129. const match = Pathmuncher.#getLowestChoiceRank(matches);
  2130. const featMatch = this.#findAllFeatureMatch(document, match.slug, { ignoreAdded });
  2131. const existingMatch = false;
  2132. // featMatch.sourceType
  2133. // ? this.parsed[featMatch.sourceType].some((f) => f.addedId === document._id)
  2134. // : false;
  2135. if (this.devMode && existingMatch) {
  2136. src_logger.warn(`Feature Choice Existing match for ${document.name}`, { featMatch, existingMatch, document });
  2137. }
  2138. // console.warn(`Match for ${document.name} featureChoiceMatch`, { match, featMatch, existingMatch, document });
  2139. if (adjustName && hasProperty(featMatch, "added") && !existingMatch) {
  2140. featMatch.added = true;
  2141. featMatch.addedId = document._id;
  2142. }
  2143. src_logger.debug("Choices evaluated", { choices, document, featMatch, match, matches });
  2144. return match.choice;
  2145. } else {
  2146. return undefined;
  2147. }
  2148. }
  2149. async #featureChoiceMatchNoUUID(document, choices, cleansedChoiceSet) {
  2150. const matches = [];
  2151. for (const choice of choices) {
  2152. const featMatch = this.#findAllFeatureMatch(document, choice.value, { ignoreAdded: true, isChoiceMatch: true });
  2153. if (featMatch) {
  2154. matches.push({
  2155. rank: featMatch.rank,
  2156. choice,
  2157. });
  2158. }
  2159. }
  2160. if (matches.length > 0) {
  2161. const match = Pathmuncher.#getLowestChoiceRank(matches);
  2162. const featMatch = this.#findAllFeatureMatch(document, match.choice.value, { ignoreAdded: true, isChoiceMatch: true });
  2163. const existingMatch = false;
  2164. // featMatch.sourceType
  2165. // ? this.parsed[featMatch.sourceType].some((f) => f.addedId === document._id)
  2166. // : false;
  2167. if (this.devMode && existingMatch) {
  2168. src_logger.warn(`NoUUID Existing match for ${document.name}`, { featMatch, existingMatch, document });
  2169. }
  2170. // console.warn(`Match for ${document.name} featureChoiceMatchNoUUID`, { match, featMatch, existingMatch, document });
  2171. if (featMatch && !existingMatch) {
  2172. featMatch.added = true;
  2173. featMatch.addedId = document._id;
  2174. match.choice.nouuid = true;
  2175. }
  2176. src_logger.debug("No UUID Choices evaluated", { choices, cleansedChoiceSet, document, featMatch, match, matches });
  2177. return match.choice;
  2178. } else {
  2179. return undefined;
  2180. }
  2181. }
  2182. static getFlag(document, ruleSet) {
  2183. return typeof ruleSet.flag === "string" && ruleSet.flag.length > 0
  2184. ? ruleSet.flag.replace(/[^-a-z0-9]/gi, "")
  2185. : Seasoning.slugD(document.system.slug ?? document.system.name ?? document.name);
  2186. }
  2187. async #evaluateChoices(document, choiceSet, choiceHint) {
  2188. src_logger.debug(`Evaluating choices for ${document.name}`, { document, choiceSet, choiceHint });
  2189. const tempActor = await this.#generateTempActor([document], false, false, true);
  2190. const cleansedChoiceSet = deepClone(choiceSet);
  2191. try {
  2192. const item = tempActor.getEmbeddedDocument("Item", document._id);
  2193. const choiceSetRules = new game.pf2e.RuleElements.all.ChoiceSet(cleansedChoiceSet, { parent: item });
  2194. const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat();
  2195. const choices = await choiceSetRules.inflateChoices(rollOptions, []);
  2196. src_logger.debug("Starting choice evaluation", {
  2197. document,
  2198. choiceSet,
  2199. item,
  2200. choiceSetRules,
  2201. rollOptions,
  2202. choices,
  2203. });
  2204. if (cleansedChoiceSet.choices?.query) {
  2205. const nonFilteredChoices = await choiceSetRules.inflateChoices(rollOptions, [item]);
  2206. const queryResults = await choiceSetRules.queryCompendium(cleansedChoiceSet.choices, rollOptions, [item]);
  2207. src_logger.debug("Query Result", { queryResults, nonFilteredChoices });
  2208. }
  2209. src_logger.debug("Evaluating choiceset", cleansedChoiceSet);
  2210. const choiceMatch = await this.#featureChoiceMatch(document, choices, true, cleansedChoiceSet.adjustName, choiceHint);
  2211. src_logger.debug("choiceMatch result", choiceMatch);
  2212. if (choiceMatch) {
  2213. choiceMatch.choiceQueryResults = deepClone(choices);
  2214. return choiceMatch;
  2215. }
  2216. if (typeof cleansedChoiceSet.choices === "string" || Array.isArray(choices)) {
  2217. const featureMatch = await this.#featureChoiceMatchNoUUID(document, choices, cleansedChoiceSet);
  2218. if (featureMatch) {
  2219. return featureMatch;
  2220. }
  2221. }
  2222. let tempSet = deepClone(choiceSet);
  2223. src_logger.debug(`Starting dynamic selection for ${document.name}`, { document, choiceSet, tempSet, Pathmuncher: this });
  2224. await choiceSetRules.preCreate({ itemSource: item, ruleSource: tempSet, pendingItems: [item], tempItems: [] });
  2225. // console.warn("chociesetdata", {
  2226. // choiceSetRules,
  2227. // selection: choiceSetRules.selection,
  2228. // choiceSet: deepClone(choiceSet),
  2229. // tempSet: deepClone(tempSet),
  2230. // });
  2231. if (tempSet.selection) {
  2232. const lookedUpChoice = choices.find((c) => c.value === tempSet.selection);
  2233. src_logger.debug("lookedUpChoice", lookedUpChoice);
  2234. if (lookedUpChoice) lookedUpChoice.choiceQueryResults = deepClone(choices);
  2235. // set some common lookups here, e.g. deities are often not set!
  2236. if (lookedUpChoice && cleansedChoiceSet.flag === "deity") {
  2237. if (lookedUpChoice.label && lookedUpChoice.label !== "") {
  2238. setProperty(this.result.character, "system.details.deity.value", lookedUpChoice.label);
  2239. await this.#processGenericCompendiumLookup("deities", lookedUpChoice.label, "deity");
  2240. const camelCase = Seasoning.slugD(this.result.deity[0].system.slug);
  2241. setProperty(document, `flags.pf2e.itemGrants.${camelCase}`, {
  2242. id: this.result.deity[0]._id,
  2243. onDelete: "detach",
  2244. });
  2245. setProperty(this.result.deity[0], "flags.pf2e.grantedBy", { id: document._id, onDelete: "cascade" });
  2246. this.autoAddedFeatureIds.add(`${lookedUpChoice.value.split(".").pop()}deity`);
  2247. }
  2248. }
  2249. return lookedUpChoice;
  2250. }
  2251. } catch (err) {
  2252. src_logger.error("Whoa! Something went major bad wrong during choice evaluation", {
  2253. err,
  2254. tempActor: tempActor.toObject(),
  2255. document: duplicate(document),
  2256. choiceSet: duplicate(cleansedChoiceSet),
  2257. });
  2258. throw err;
  2259. } finally {
  2260. await Actor.deleteDocuments([tempActor._id]);
  2261. }
  2262. src_logger.debug("Evaluate Choices failed", { choiceSet: cleansedChoiceSet, tempActor, document });
  2263. return undefined;
  2264. }
  2265. async #resolveInjectedUuid(document, ruleEntry) {
  2266. const tempActor = await this.#generateTempActor([document], false, false);
  2267. const cleansedRuleEntry = deepClone(ruleEntry);
  2268. try {
  2269. const item = tempActor.getEmbeddedDocument("Item", document._id);
  2270. // console.warn("creating grant item");
  2271. const grantItemRule = new game.pf2e.RuleElements.all.GrantItem(cleansedRuleEntry, { parent: item });
  2272. // console.warn("Begining uuid resovle");
  2273. const uuid = grantItemRule.resolveInjectedProperties(grantItemRule.uuid, { warn: false });
  2274. src_logger.debug("uuid selection", {
  2275. document,
  2276. choiceSet: ruleEntry,
  2277. item,
  2278. grantItemRule,
  2279. uuid,
  2280. });
  2281. if (uuid) return uuid;
  2282. } catch (err) {
  2283. src_logger.error("Whoa! Something went major bad wrong during uuid evaluation", {
  2284. err,
  2285. tempActor: tempActor.toObject(),
  2286. document: duplicate(document),
  2287. ruleEntry: duplicate(cleansedRuleEntry),
  2288. });
  2289. throw err;
  2290. } finally {
  2291. await Actor.deleteDocuments([tempActor._id]);
  2292. }
  2293. src_logger.debug("Evaluate UUID failed", { choiceSet: cleansedRuleEntry, tempActor, document });
  2294. return undefined;
  2295. }
  2296. async #checkRule(document, rule) {
  2297. const tempActor = await this.#generateTempActor([document], true);
  2298. const cleansedRule = deepClone(rule);
  2299. try {
  2300. const item = tempActor.getEmbeddedDocument("Item", document._id);
  2301. const ruleElement = cleansedRule.key === "ChoiceSet"
  2302. ? new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, { parent: item })
  2303. : new game.pf2e.RuleElements.all.GrantItem(cleansedRule, { parent: item });
  2304. const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat();
  2305. if (rule.predicate) {
  2306. const predicate = ruleElement.resolveInjectedProperties(ruleElement.predicate);
  2307. if (!predicate.test(rollOptions)) return false;
  2308. }
  2309. const choices = cleansedRule.key === "ChoiceSet"
  2310. ? await ruleElement.inflateChoices(rollOptions, [item])
  2311. : [ruleElement.resolveValue()];
  2312. const isGood = cleansedRule.key === "ChoiceSet"
  2313. ? (await this.#featureChoiceMatch(document, choices, false)) !== undefined
  2314. : ruleElement.test(rollOptions);
  2315. src_logger.debug("Checking rule", {
  2316. tempActor,
  2317. cleansedRule,
  2318. item,
  2319. ruleElement,
  2320. rollOptions,
  2321. choices,
  2322. isGood,
  2323. });
  2324. return isGood;
  2325. } catch (err) {
  2326. src_logger.error("Something has gone most wrong during rule checking", {
  2327. document,
  2328. rule: cleansedRule,
  2329. tempActor,
  2330. });
  2331. throw err;
  2332. } finally {
  2333. await Actor.deleteDocuments([tempActor._id]);
  2334. }
  2335. }
  2336. async #checkRulePredicate(document, rule) {
  2337. const tempActor = await this.#generateTempActor([document], true);
  2338. const cleansedRule = deepClone(rule);
  2339. try {
  2340. const item = tempActor.getEmbeddedDocument("Item", document._id);
  2341. const ruleElement = cleansedRule.key === "ChoiceSet"
  2342. ? new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, { parent: item })
  2343. : new game.pf2e.RuleElements.all.GrantItem(cleansedRule, { parent: item });
  2344. const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat();
  2345. if (rule.predicate) {
  2346. const predicate = ruleElement.resolveInjectedProperties(ruleElement.predicate);
  2347. return predicate.test(rollOptions);
  2348. } else {
  2349. return true;
  2350. }
  2351. } catch (err) {
  2352. src_logger.error("Something has gone most wrong during rule predicate checking", {
  2353. document,
  2354. rule: cleansedRule,
  2355. tempActor,
  2356. });
  2357. throw err;
  2358. } finally {
  2359. await Actor.deleteDocuments([tempActor._id]);
  2360. }
  2361. }
  2362. static adjustDocumentName(featureName, label) {
  2363. const localLabel = game.i18n.localize(label);
  2364. if (featureName.trim().toLowerCase() === localLabel.trim().toLowerCase()) return featureName;
  2365. const name = `${featureName} (${localLabel})`;
  2366. const pattern = (() => {
  2367. const escaped = RegExp.escape(localLabel);
  2368. return new RegExp(`\\(${escaped}\\) \\(${escaped}\\)$`);
  2369. })();
  2370. return name.replace(pattern, `(${localLabel})`);
  2371. }
  2372. // eslint-disable-next-line complexity, no-unused-vars
  2373. async #addGrantedRules(document, originType = null, choiceHint = null) {
  2374. if (document.system.rules.length === 0) return;
  2375. src_logger.debug(`addGrantedRules for ${document.name}`, duplicate(document));
  2376. if (
  2377. hasProperty(document, "system.level.value")
  2378. && document.system.level.value > this.result.character.system.details.level.value
  2379. ) {
  2380. return;
  2381. }
  2382. const rulesToKeep = [];
  2383. this.allFeatureRules[document._id] = deepClone(document.system.rules);
  2384. this.autoAddedFeatureRules[document._id] = [];
  2385. this.promptRules[document._id] = [];
  2386. let featureRenamed = false;
  2387. for (const ruleEntry of document.system.rules) {
  2388. src_logger.debug(`Ping ${document.name} rule key: ${ruleEntry.key}`, ruleEntry);
  2389. if (!["ChoiceSet", "GrantItem"].includes(ruleEntry.key)) {
  2390. // size work around due to Pathbuilder not always adding the right size to json
  2391. if (ruleEntry.key === "CreatureSize") this.size = ruleEntry.value;
  2392. this.autoAddedFeatureRules[document._id].push(ruleEntry);
  2393. rulesToKeep.push(ruleEntry);
  2394. continue;
  2395. }
  2396. if (NO_AUTO_CHOICE().includes(document.name)) {
  2397. src_logger.debug(`Deliberately skipping ${document.name} auto choice detection`);
  2398. rulesToKeep.push(ruleEntry);
  2399. continue;
  2400. }
  2401. src_logger.debug(`Checking ${document.name} rule key: ${ruleEntry.key}`, {
  2402. ruleEntry,
  2403. docRules: deepClone(document.system.rules),
  2404. document: deepClone(document),
  2405. });
  2406. if (ruleEntry.key === "ChoiceSet" && ruleEntry.predicate) {
  2407. src_logger.debug(`Checking for predicates`, {
  2408. ruleEntry,
  2409. document,
  2410. });
  2411. const testResult = await this.#checkRulePredicate(duplicate(document), ruleEntry);
  2412. if (!testResult) {
  2413. const data = { document, ruleEntry, testResult };
  2414. src_logger.debug(
  2415. `The test failed for ${document.name} rule key: ${ruleEntry.key} (This is probably not a problem).`,
  2416. data
  2417. );
  2418. rulesToKeep.push(ruleEntry);
  2419. continue;
  2420. }
  2421. }
  2422. const choice = ruleEntry.key === "ChoiceSet" ? await this.#evaluateChoices(document, ruleEntry, choiceHint) : undefined;
  2423. const uuid = ruleEntry.key === "GrantItem" ? await this.#resolveInjectedUuid(document, ruleEntry) : choice?.value;
  2424. if (choice?.choiceQueryResults) {
  2425. ruleEntry.choiceQueryResults = choice.choiceQueryResults;
  2426. }
  2427. const flagName = Pathmuncher.getFlag(document, ruleEntry);
  2428. // if (flagName && choice?.value && !hasProperty(document, `flags.pf2e.rulesSelections.${flagName}`)) {
  2429. // setProperty(document, `flags.pf2e.rulesSelections.${flagName}`, choice.value);
  2430. // }
  2431. src_logger.debug(`UUID for ${document.name}: "${uuid}"`, { document, ruleEntry, choice, uuid });
  2432. const ruleFeature = uuid && typeof uuid === "string" ? await fromUuid(uuid) : undefined;
  2433. // console.warn("ruleFeature", ruleFeature);
  2434. if (ruleFeature) {
  2435. const featureDoc = ruleFeature.toObject();
  2436. featureDoc._id = foundry.utils.randomID();
  2437. if (featureDoc.system.rules) this.allFeatureRules[featureDoc._id] = deepClone(featureDoc.system.rules);
  2438. setProperty(featureDoc, "flags.pathmuncher.origin.uuid", uuid);
  2439. src_logger.debug(`Found rule feature ${featureDoc.name} for ${document.name} for`, ruleEntry);
  2440. if (choice) {
  2441. ruleEntry.selection = choice.value;
  2442. setProperty(document, `flags.pf2e.rulesSelections.${flagName}`, choice.value);
  2443. }
  2444. if (src_utils.isString(ruleEntry.rollOption)) {
  2445. ruleEntry.rollOption = `${ruleEntry.rollOption}:${flagName}`;
  2446. }
  2447. if (ruleEntry.predicate && ruleEntry.key === "GrantItem") {
  2448. src_logger.debug(`Checking for grantitem predicates`, {
  2449. ruleEntry,
  2450. document,
  2451. featureDoc,
  2452. });
  2453. const testResult = await this.#checkRule(featureDoc, ruleEntry);
  2454. if (!testResult) {
  2455. const data = { document, ruleEntry, featureDoc, testResult };
  2456. src_logger.debug(
  2457. `The test failed for ${document.name} rule key: ${ruleEntry.key} (This is probably not a problem).`,
  2458. data
  2459. );
  2460. rulesToKeep.push(ruleEntry);
  2461. // this.autoAddedFeatureRules[document._id].push(ruleEntry);
  2462. continue;
  2463. } else {
  2464. src_logger.debug(`The test passed for ${document.name} rule key: ${ruleEntry.key}`, ruleEntry);
  2465. // this.autoAddedFeatureRules[document._id].push(ruleEntry);
  2466. // eslint-disable-next-line max-depth
  2467. // if (!ruleEntry.flag) ruleEntry.flag = Seasoning.slugD(document.name);
  2468. ruleEntry.pathmuncherImport = true;
  2469. rulesToKeep.push(ruleEntry);
  2470. }
  2471. }
  2472. // setProperty(ruleEntry, `preselectChoices.${ruleEntry.flag}`, ruleEntry.selection ?? ruleEntry.uuid);
  2473. if (this.autoAddedFeatureIds.has(`${ruleFeature.id}${ruleFeature.type}`)) {
  2474. src_logger.debug(`Feature ${featureDoc.name} found for ${document.name}, but has already been added (${ruleFeature.id})`, ruleFeature);
  2475. // this.autoAddedFeatureRules[document._id].push(ruleEntry);
  2476. // rulesToKeep.push(ruleEntry);
  2477. if (ruleEntry.key === "GrantItem" && ruleEntry.flag) {
  2478. this.autoAddedFeatureRules[document._id].push(ruleEntry);
  2479. rulesToKeep.push(ruleEntry);
  2480. }
  2481. continue;
  2482. } else {
  2483. src_logger.debug(`Feature ${featureDoc.name} not found for ${document.name}, adding (${ruleFeature.id})`, ruleFeature);
  2484. if (ruleEntry.selection || ruleEntry.flag) {
  2485. rulesToKeep.push(ruleEntry);
  2486. }
  2487. this.autoAddedFeatureIds.add(`${ruleFeature.id}${ruleFeature.type}`);
  2488. featureDoc._id = foundry.utils.randomID();
  2489. this.#createGrantedItem(featureDoc, document, { itemGrantName: flagName, applyFeatLocation: false });
  2490. if (hasProperty(featureDoc, "system.rules")) await this.#addGrantedRules(featureDoc);
  2491. }
  2492. } else if (getProperty(choice, "nouuid")) {
  2493. src_logger.debug("Parsed no id rule", { choice, uuid, ruleEntry });
  2494. if (!ruleEntry.flag) ruleEntry.flag = Seasoning.slugD(document.name);
  2495. ruleEntry.selection = choice.value;
  2496. if (choice.label) document.name = `${document.name} (${choice.label})`;
  2497. rulesToKeep.push(ruleEntry);
  2498. } else if (choice && uuid && !hasProperty(ruleEntry, "selection")) {
  2499. src_logger.debug("Parsed odd choice rule", { choice, uuid, ruleEntry });
  2500. // if (!ruleEntry.flag) ruleEntry.flag = Seasoning.slugD(document.name);
  2501. ruleEntry.selection = choice.value;
  2502. if (
  2503. ((!ruleEntry.adjustName && choice.label && typeof uuid === "object")
  2504. || (!choice.adjustName && choice.label))
  2505. && !featureRenamed
  2506. ) {
  2507. document.name = Pathmuncher.adjustDocumentName(document.name, choice.label);
  2508. featureRenamed = true;
  2509. }
  2510. rulesToKeep.push(ruleEntry);
  2511. } else {
  2512. src_logger.debug(`Final rule fallback for ${document.name}`, ruleEntry);
  2513. const data = {
  2514. uuid: ruleEntry.uuid,
  2515. document,
  2516. ruleEntry,
  2517. choice,
  2518. };
  2519. if (
  2520. ruleEntry.key === "GrantItem"
  2521. && (ruleEntry.flag || ruleEntry.selection || ruleEntry.uuid.startsWith("Compendium"))
  2522. ) {
  2523. rulesToKeep.push(ruleEntry);
  2524. } else if (ruleEntry.key === "ChoiceSet" && !hasProperty(ruleEntry, "flag")) {
  2525. src_logger.debug("Prompting user for choices", ruleEntry);
  2526. this.promptRules[document._id].push(ruleEntry);
  2527. rulesToKeep.push(ruleEntry);
  2528. } else if (ruleEntry.key === "ChoiceSet" && !choice && !uuid) {
  2529. src_logger.warn("Unable to determine choice asking", data);
  2530. rulesToKeep.push(ruleEntry);
  2531. this.promptRules[document._id].push(ruleEntry);
  2532. }
  2533. src_logger.warn("Unable to determine granted rule feature, needs better parser", data);
  2534. }
  2535. if (ruleEntry.adjustName && choice?.label && !featureRenamed) {
  2536. document.name = Pathmuncher.adjustDocumentName(document.name, choice.label);
  2537. }
  2538. this.autoAddedFeatureRules[document._id].push(ruleEntry);
  2539. src_logger.debug(`End result for ${document.name} for a ${ruleEntry.key}`, {
  2540. document: deepClone(document),
  2541. rulesToKeep: deepClone(rulesToKeep),
  2542. ruleEntry: deepClone(ruleEntry),
  2543. choice: deepClone(choice),
  2544. uuid: deepClone(uuid),
  2545. });
  2546. }
  2547. // eslint-disable-next-line require-atomic-updates
  2548. document.system.rules = rulesToKeep;
  2549. src_logger.debug(`Final status for ${document.name}`, {
  2550. document: deepClone(document),
  2551. rulesToKeep: deepClone(rulesToKeep),
  2552. });
  2553. }
  2554. async #addGrantedItems(document, { originType = null, applyFeatLocation = false, choiceHint = null } = {}) {
  2555. const immediateDiveAdd = src_utils.setting("USE_IMMEDIATE_DEEP_DIVE");
  2556. const subRuleDocuments = [];
  2557. if (hasProperty(document, "system.items")) {
  2558. src_logger.debug(`addGrantedItems for ${document.name}`, duplicate(document));
  2559. if (!this.autoAddedFeatureItems[document._id]) {
  2560. this.autoAddedFeatureItems[document._id] = duplicate(document.system.items);
  2561. }
  2562. const failedFeatureItems = {};
  2563. for (const [key, grantedItemFeature] of Object.entries(document.system.items).sort(([, a], [, b]) => a.level - b.level)) {
  2564. src_logger.debug(`Checking ${document.name} granted item ${grantedItemFeature.name}, level(${grantedItemFeature.level}) with key: ${key}`, grantedItemFeature);
  2565. if (grantedItemFeature.level > getProperty(this.result.character, "system.details.level.value")) continue;
  2566. const feature = await fromUuid(grantedItemFeature.uuid);
  2567. if (!feature) {
  2568. const data = { uuid: grantedItemFeature.uuid, grantedFeature: grantedItemFeature, feature };
  2569. src_logger.warn("Unable to determine granted item feature, needs better parser", data);
  2570. failedFeatureItems[key] = grantedItemFeature;
  2571. continue;
  2572. }
  2573. this.autoAddedFeatureIds.add(`${feature.id}${feature.type}`);
  2574. const featureDoc = feature.toObject();
  2575. featureDoc._id = foundry.utils.randomID();
  2576. setProperty(featureDoc.system, "location", document._id);
  2577. this.#createGrantedItem(featureDoc, document, { originType, applyFeatLocation });
  2578. if (hasProperty(featureDoc, "system.rules")) {
  2579. src_logger.debug(`Processing granted rules for granted item document ${featureDoc.name}`, duplicate(featureDoc));
  2580. if (immediateDiveAdd) {
  2581. await this.#addGrantedItems(featureDoc, { originType, applyFeatLocation });
  2582. } else {
  2583. subRuleDocuments.push(featureDoc);
  2584. }
  2585. }
  2586. }
  2587. // eslint-disable-next-line require-atomic-updates
  2588. document.system.items = failedFeatureItems;
  2589. if (!immediateDiveAdd) {
  2590. for (const subRuleDocument of subRuleDocuments) {
  2591. src_logger.debug(
  2592. `Processing granted rules for granted item document ${subRuleDocument.name}`,
  2593. duplicate(subRuleDocument)
  2594. );
  2595. await this.#addGrantedItems(subRuleDocument, { originType, applyFeatLocation, choiceHint });
  2596. }
  2597. }
  2598. }
  2599. if (hasProperty(document, "system.rules")) {
  2600. src_logger.debug(`Processing granted rules for core document ${document.name}`, duplicate(document));
  2601. await this.#addGrantedRules(document, originType, choiceHint);
  2602. }
  2603. }
  2604. #determineAbilityBoosts() {
  2605. const breakdown = getProperty(this.source, "abilities.breakdown");
  2606. const useCustomStats
  2607. = breakdown
  2608. && breakdown.ancestryFree.length === 0
  2609. && breakdown.ancestryBoosts.length === 0
  2610. && breakdown.ancestryFlaws.length === 0
  2611. && breakdown.backgroundBoosts.length === 0
  2612. && breakdown.classBoosts.length === 0;
  2613. if (breakdown && !useCustomStats) {
  2614. this.boosts.custom = false;
  2615. const classBoostMap = {};
  2616. for (const [key, boosts] of Object.entries(this.source.abilities.breakdown.mapLevelledBoosts)) {
  2617. if (key <= this.source.level) {
  2618. classBoostMap[key] = boosts.map((ability) => ability.toLowerCase());
  2619. }
  2620. }
  2621. setProperty(this.result.character, "system.build.attributes.boosts", classBoostMap);
  2622. this.boosts.class = classBoostMap;
  2623. // ancestry
  2624. } else {
  2625. this.boosts.custom = true;
  2626. ["str", "dex", "con", "int", "wis", "cha"].forEach((key) => {
  2627. const mod = Math.min(Math.max(Math.trunc((this.source.abilities[key] - 10) / 2), -5), 10) || 0;
  2628. setProperty(this.result.character, `system.abilities.${key}.mod`, mod);
  2629. });
  2630. }
  2631. if (breakdown?.classBoosts.length > 0) {
  2632. this.keyAbility = breakdown.classBoosts[0].toLowerCase();
  2633. } else {
  2634. this.keyAbility = this.source.keyability;
  2635. }
  2636. setProperty(this.result.character, "system.details.keyability.value", this.keyAbility);
  2637. }
  2638. #generateBackgroundAbilityBoosts() {
  2639. if (!this.result.background[0]) return;
  2640. const breakdown = getProperty(this.source, "abilities.breakdown");
  2641. for (const boost of breakdown.backgroundBoosts) {
  2642. for (const [key, boostSet] of Object.entries(this.result.background[0].system.boosts)) {
  2643. if (this.result.background[0].system.boosts[key].selected) continue;
  2644. if (boostSet.value.includes(boost.toLowerCase())) {
  2645. this.result.background[0].system.boosts[key].selected = boost.toLowerCase();
  2646. break;
  2647. }
  2648. }
  2649. }
  2650. }
  2651. #generateAncestryAbilityBoosts() {
  2652. if (!this.result.ancestry[0]) return;
  2653. const breakdown = getProperty(this.source, "abilities.breakdown");
  2654. const boosts = [];
  2655. breakdown.ancestryBoosts.concat(breakdown.ancestryFree).forEach((boost) => {
  2656. for (const [key, boostSet] of Object.entries(this.result.ancestry[0].system.boosts)) {
  2657. if (this.result.ancestry[0].system.boosts[key].selected) continue;
  2658. if (boostSet.value.includes(boost.toLowerCase())) {
  2659. this.result.ancestry[0].system.boosts[key].selected = boost.toLowerCase();
  2660. boosts.push(boost.toLowerCase());
  2661. break;
  2662. }
  2663. }
  2664. });
  2665. if (breakdown.ancestryBoosts.length === 0) {
  2666. setProperty(this.result.ancestry[0], "system.alternateAncestryBoosts", boosts);
  2667. }
  2668. }
  2669. #setAbilityBoosts() {
  2670. if (this.boosts.custom) return;
  2671. this.#generateBackgroundAbilityBoosts();
  2672. this.#generateAncestryAbilityBoosts();
  2673. this.result.class[0].system.boosts = this.boosts.class;
  2674. }
  2675. static SKILL_LOOKUP = {
  2676. "acrobatics": "acr",
  2677. "arcana": "arc",
  2678. "athletics": "ath",
  2679. "crafting": "cra",
  2680. "deception": "dec",
  2681. "diplomacy": "dip",
  2682. "intimidation": "itm",
  2683. "medicine": "med",
  2684. "nature": "nat",
  2685. "occultism": "occ",
  2686. "performance": "prf",
  2687. "religion": "rel",
  2688. "society": "soc",
  2689. "stealth": "ste",
  2690. "survival": "sur",
  2691. "thievery": "thi",
  2692. };
  2693. #setSkills(removeSpecials = false) {
  2694. for (const [key, value] of Object.entries(Pathmuncher.SKILL_LOOKUP)) {
  2695. const calculatedValue = removeSpecials
  2696. && (this.source.specials.some((s) => s.toLowerCase() === key)
  2697. || this.parsed.specials.some((s) => s.name.toLowerCase() === key))
  2698. ? 0
  2699. : this.source.proficiencies[key] / 2;
  2700. setProperty(this.result.character, `system.skills.${value}.rank`, calculatedValue);
  2701. };
  2702. }
  2703. #setSaves() {
  2704. ["fortitude", "reflex", "will"].forEach((key) => {
  2705. setProperty(this.result.character, `system.savingThrows.${key}`, this.source.proficiencies[key] / 2);
  2706. });
  2707. }
  2708. #setMartials() {
  2709. ["advanced", "heavy", "light", "medium", "unarmored", "martial", "simple", "unarmed"].forEach((key) => {
  2710. setProperty(this.result.character, `system.martial.${key}.rank`, this.source.proficiencies[key] / 2);
  2711. });
  2712. }
  2713. async #processCore() {
  2714. setProperty(this.result.character, "name", this.source.name);
  2715. setProperty(this.result.character, "prototypeToken.name", this.source.name);
  2716. setProperty(this.result.character, "system.details.level.value", this.source.level);
  2717. if (this.source.age !== "Not set") setProperty(this.result.character, "system.details.age.value", this.source.age);
  2718. if (this.source.gender !== "Not set") setProperty(this.result.character, "system.details.gender.value", this.source.gender);
  2719. // setProperty(this.result.character, "system.details.alignment.value", this.source.alignment);
  2720. if (this.source.deity !== "Not set") setProperty(this.result.character, "system.details.deity.value", this.source.deity);
  2721. this.size = Seasoning.getSizeValue(this.source.size);
  2722. setProperty(this.result.character, "system.traits.size.value", this.size);
  2723. setProperty(this.result.character, "system.traits.languages.value", this.source.languages.map((l) => l.toLowerCase()));
  2724. this.#processSenses();
  2725. this.#determineAbilityBoosts();
  2726. this.#setSaves();
  2727. this.#setMartials();
  2728. // setProperty(this.result.character, "system.attributes.perception.rank", this.source.proficiencies.perception / 2);
  2729. // setProperty(this.result.character, "system.attributes.classDC.rank", this.source.proficiencies.classDC / 2);
  2730. }
  2731. #indexFind(index, arrayOfNameMatches) {
  2732. for (const name of arrayOfNameMatches) {
  2733. const indexMatch = index.find((i) => {
  2734. const slug = i.system.slug ?? Seasoning.slug(i.name);
  2735. return (
  2736. slug === Seasoning.slug(name)
  2737. || slug === Seasoning.slug(Seasoning.getClassAdjustedSpecialNameLowerCase(name, this.source.class))
  2738. || slug === Seasoning.slug(Seasoning.getAncestryAdjustedSpecialNameLowerCase(name, this.source.ancestry))
  2739. || slug === Seasoning.slug(Seasoning.getHeritageAdjustedSpecialNameLowerCase(name, this.source.heritage))
  2740. || (src_utils.allowDualClasses()
  2741. && slug === Seasoning.slug(Seasoning.getDualClassAdjustedSpecialNameLowerCase(name, this.source.dualClass)))
  2742. );
  2743. });
  2744. if (indexMatch) return indexMatch;
  2745. }
  2746. return undefined;
  2747. }
  2748. #findInPackIndexes(type, arrayOfNameMatches) {
  2749. const matcher = this.compendiumMatchers[type];
  2750. for (const [packName, index] of Object.entries(matcher.indexes)) {
  2751. const indexMatch = this.#indexFind(index, arrayOfNameMatches);
  2752. if (indexMatch) return { i: indexMatch, pack: matcher.packs[packName] };
  2753. }
  2754. return undefined;
  2755. }
  2756. #sortParsedFeats() {
  2757. // eslint-disable-next-line complexity
  2758. this.parsed.feats.sort((f1, f2) => {
  2759. const f1RefUndefined = !(typeof f1.type === "string" || f1.type instanceof String);
  2760. const f2RefUndefined = !(typeof f2.type === "string" || f2.type instanceof String);
  2761. if (f1RefUndefined || f2RefUndefined) {
  2762. if (f1RefUndefined && f2RefUndefined) {
  2763. return 0;
  2764. } else if (f1RefUndefined) {
  2765. return 1;
  2766. } else {
  2767. return -1;
  2768. }
  2769. } else if (f1.type === "Awarded Feat" && f2.type === "Awarded Feat") {
  2770. return (f1.level ?? 20) - (f2.level ?? 20);
  2771. } else if (f1.type === "Awarded Feat") {
  2772. return 1;
  2773. } else if (f2.type === "Awarded Feat") {
  2774. return -1;
  2775. } else if ((f1.level ?? 20) === (f2.level ?? 20)) {
  2776. const f1Index = constants.FEAT_PRIORITY.indexOf(f1.type);
  2777. const f2Index = constants.FEAT_PRIORITY.indexOf(f2.type);
  2778. if (f1Index > f2Index) {
  2779. return 1;
  2780. } else if (f1Index < f2Index) {
  2781. return -1;
  2782. } else {
  2783. return 0;
  2784. }
  2785. } else {
  2786. return (f1.level ?? 20) - (f2.level ?? 20);
  2787. }
  2788. });
  2789. }
  2790. // eslint-disable-next-line complexity
  2791. async #generateFeatItems(type,
  2792. { levelCap = null, typeFilter = null, excludeChild = false, excludeParents = false, excludeStandard = false } = {}
  2793. ) {
  2794. src_logger.debug(`Generate feat items for ${type} with level cap "${levelCap}" and filter "${typeFilter}"`);
  2795. for (const featArray of [this.parsed.feats, this.parsed.specials]) {
  2796. for (const pBFeat of featArray) {
  2797. if (pBFeat.added) continue;
  2798. if (levelCap && (pBFeat.level ?? 20) > levelCap) continue;
  2799. if (typeFilter && pBFeat.type !== typeFilter) continue;
  2800. if (excludeChild && pBFeat.isChild === true) continue;
  2801. if (excludeParents && pBFeat.isParent === true) continue;
  2802. if (excludeStandard && pBFeat.isStandard === true) continue;
  2803. src_logger.debug(`Generating feature for ${pBFeat.name}`, pBFeat);
  2804. if (this.devMode) src_logger.error(`Generating feature for ${pBFeat.name}`, { pBFeat, this: this });
  2805. const indexMatch = this.#findInPackIndexes(type, [pBFeat.name, pBFeat.originalName]);
  2806. const displayName = pBFeat.extra ? Pathmuncher.adjustDocumentName(pBFeat.name, pBFeat.extra) : pBFeat.name;
  2807. if (!indexMatch) {
  2808. src_logger.debug(`Unable to match feat ${displayName}`, {
  2809. displayName,
  2810. name: pBFeat.name,
  2811. extra: pBFeat.extra,
  2812. pBFeat,
  2813. type,
  2814. });
  2815. this.check[pBFeat.originalName] = {
  2816. name: displayName,
  2817. type: "feat",
  2818. details: {
  2819. displayName,
  2820. name: pBFeat.name,
  2821. originalName: pBFeat.originalName,
  2822. extra: pBFeat.extra,
  2823. pBFeat,
  2824. type,
  2825. },
  2826. };
  2827. continue;
  2828. }
  2829. if (this.check[pBFeat.originalName]) delete this.check[pBFeat.originalName];
  2830. pBFeat.added = true;
  2831. if (this.autoAddedFeatureIds.has(`${indexMatch._id}${indexMatch.type}`)) {
  2832. src_logger.debug("Feat included in class features auto add", { displayName, pBFeat, type });
  2833. pBFeat.addedAutoId = `${indexMatch._id}_${indexMatch.type}`;
  2834. continue;
  2835. }
  2836. const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  2837. const docData = doc.toObject();
  2838. docData._id = foundry.utils.randomID();
  2839. pBFeat.addedId = docData._id;
  2840. // docData.name = displayName;
  2841. this.#generateFoundryFeatLocation(docData, pBFeat);
  2842. this.result.feats.push(docData);
  2843. const options = {
  2844. originType: typeFilter,
  2845. applyFeatLocation: false,
  2846. choiceHint: pBFeat.extra && pBFeat.extra !== "" ? pBFeat.extra : null,
  2847. };
  2848. await this.#addGrantedItems(docData, "feat", options);
  2849. }
  2850. }
  2851. }
  2852. async #generateSpecialItems(type) {
  2853. for (const special of this.parsed.specials) {
  2854. if (special.added) continue;
  2855. src_logger.debug("Generating special for", special);
  2856. const indexMatch = this.#findInPackIndexes(type, [special.name, special.originalName]);
  2857. if (!indexMatch) {
  2858. src_logger.debug(`Unable to match special ${special.name}`, { special: special.name, type });
  2859. this.check[special.originalName] = {
  2860. name: special.name,
  2861. type: "special",
  2862. details: { displayName: special.name, name: special.name, originalName: special.originalName, special },
  2863. };
  2864. continue;
  2865. }
  2866. special.added = true;
  2867. if (this.check[special.originalName]) delete this.check[special.originalName];
  2868. if (this.autoAddedFeatureIds.has(`${indexMatch._id}${indexMatch.type}`)) {
  2869. src_logger.debug("Special included in class features auto add", { special: special.name, type });
  2870. special.addedAutoId = `${indexMatch._id}_${indexMatch.type}`;
  2871. continue;
  2872. }
  2873. const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  2874. const docData = doc.toObject();
  2875. docData._id = foundry.utils.randomID();
  2876. special.addedId = docData._id;
  2877. this.result.feats.push(docData);
  2878. await this.#addGrantedItems(docData, { applyFeatLocation: true });
  2879. }
  2880. }
  2881. #resizeItem(item) {
  2882. if (Seasoning.isPhysicalItemType(item.type)) {
  2883. const resizeItem = item.type !== "treasure" && !["med", "sm"].includes(this.size);
  2884. if (resizeItem) item.system.size = this.size;
  2885. }
  2886. }
  2887. async #generateAdventurersPack() {
  2888. const defaultCompendium = game.packs.get("pf2e.equipment-srd");
  2889. const index = await defaultCompendium.getIndex({ fields: ["name", "type", "system.slug"] });
  2890. const adventurersPack = this.parsed.equipment.find((e) => e.pbName === "Adventurer's Pack");
  2891. if (adventurersPack) {
  2892. const compendiumBackpack = await defaultCompendium.getDocument("3lgwjrFEsQVKzhh7");
  2893. const backpackInstance = compendiumBackpack.toObject();
  2894. adventurersPack.added = true;
  2895. backpackInstance._id = foundry.utils.randomID();
  2896. adventurersPack.addedId = backpackInstance._id;
  2897. this.result.adventurersPack.item = adventurersPack;
  2898. this.result.equipment.push(backpackInstance);
  2899. for (const content of this.result.adventurersPack.contents) {
  2900. const indexMatch = index.find((i) => i.system.slug === content.slug);
  2901. if (!indexMatch) {
  2902. src_logger.error(`Unable to match adventurers kit item ${content.name}`, content);
  2903. continue;
  2904. }
  2905. const doc = await defaultCompendium.getDocument(indexMatch._id);
  2906. const itemData = doc.toObject();
  2907. itemData._id = foundry.utils.randomID();
  2908. itemData.system.quantity = content.qty;
  2909. itemData.system.containerId = backpackInstance?._id;
  2910. this.#resizeItem(itemData);
  2911. this.result.equipment.push(itemData);
  2912. }
  2913. }
  2914. }
  2915. async #generateContainers() {
  2916. for (const [key, data] of Object.entries(this.source.equipmentContainers)) {
  2917. if (data.foundryId) continue;
  2918. const name = Seasoning.getFoundryEquipmentName(data.containerName);
  2919. const indexMatch = this.compendiumMatchers["equipment"].getMatch(data.containerName, name);
  2920. const id = foundry.utils.randomID();
  2921. const doc = indexMatch
  2922. ? await indexMatch.pack.getDocument(indexMatch.i._id)
  2923. : await Item.create({ name: data.containerName, type: "backpack" }, { temporary: true });
  2924. const itemData = doc.toObject();
  2925. itemData._id = id;
  2926. this.#resizeItem(itemData);
  2927. this.result["equipment"].push(itemData);
  2928. this.parsed.equipment.push({
  2929. foundryName: name,
  2930. pbName: data.containerName,
  2931. originalName: data.containerName,
  2932. name,
  2933. qty: 1,
  2934. added: true,
  2935. inContainer: undefined,
  2936. container: this.#getContainerData(key),
  2937. foundryId: id,
  2938. });
  2939. }
  2940. }
  2941. async #generateEquipmentItems() {
  2942. for (const e of this.parsed.equipment) {
  2943. if (e.pbName === "Adventurer's Pack") continue;
  2944. if (e.added) continue;
  2945. if (Seasoning.IGNORED_EQUIPMENT().includes(e.pbName)) {
  2946. e.added = true;
  2947. e.addedAutoId = "ignored";
  2948. continue;
  2949. }
  2950. src_logger.debug("Generating item for", e);
  2951. const indexMatch = this.compendiumMatchers["equipment"].getMatch(e.pbName, e.foundryName);
  2952. if (!indexMatch) {
  2953. src_logger.error(`Unable to match ${e.pbName}`, e);
  2954. this.bad.push({ pbName: e.pbName, type: "equipment", details: { e } });
  2955. continue;
  2956. }
  2957. const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  2958. if (doc.type != "kit") {
  2959. const itemData = doc.toObject();
  2960. itemData._id = e.foundryId || foundry.utils.randomID();
  2961. itemData.system.quantity = e.qty;
  2962. const type = doc.type === "treasure" ? "treasure" : "equipment";
  2963. if (e.inContainer) {
  2964. const containerMatch = this.parsed.equipment.find((con) => con.container?.id === e.inContainer);
  2965. if (containerMatch) {
  2966. itemData.system.containerId = containerMatch.foundryId;
  2967. itemData.system.equipped.carryType = "stowed";
  2968. }
  2969. }
  2970. if (e.invested) {
  2971. itemData.system.equipped.carryType = "worn";
  2972. itemData.system.equipped.invested = true;
  2973. itemData.system.equipped.inSlot = true;
  2974. itemData.system.equipped.handsHeld = 0;
  2975. }
  2976. this.#resizeItem(itemData);
  2977. this.result[type].push(itemData);
  2978. e.addedId = itemData._id;
  2979. }
  2980. // eslint-disable-next-line require-atomic-updates
  2981. e.added = true;
  2982. }
  2983. }
  2984. async #processEquipmentItems() {
  2985. // just in case it's in the equipment, pathbuilder should have translated this to items
  2986. await this.#generateAdventurersPack();
  2987. await this.#generateContainers();
  2988. await this.#generateEquipmentItems();
  2989. }
  2990. static RUNE_SCALE = [
  2991. "",
  2992. "Minor",
  2993. "Lesser",
  2994. "Moderate",
  2995. "Greater",
  2996. "Major",
  2997. "Supreme",
  2998. ];
  2999. static REINFORCING_DATA = {
  3000. "Minor": {
  3001. value: 1,
  3002. hp: 44,
  3003. },
  3004. "Lesser": {
  3005. value: 2,
  3006. hp: 52,
  3007. },
  3008. "Moderate": {
  3009. value: 3,
  3010. hp: 64,
  3011. },
  3012. "Greater": {
  3013. value: 4,
  3014. hp: 80,
  3015. },
  3016. "Major": {
  3017. value: 5,
  3018. hp: 84,
  3019. },
  3020. "Supreme": {
  3021. value: 6,
  3022. hp: 108,
  3023. },
  3024. };
  3025. static POTENCY_SCALE = [
  3026. "",
  3027. "striking",
  3028. "greaterStriking",
  3029. "majorStriking",
  3030. ];
  3031. static RESILIENT_SCALE = [
  3032. "",
  3033. "resilient",
  3034. "greaterResilient",
  3035. "majorResilient",
  3036. ];
  3037. // eslint-disable-next-line complexity
  3038. static applyRunes(parsedItem, itemData, type) {
  3039. if (itemData.type == "shield") {
  3040. parsedItem.runes.forEach((rune) => {
  3041. if (rune.startsWith("Reinforcing")) {
  3042. const runeScale = rune.split("(").pop().split(")").shift().trim();
  3043. const runeMatch = Pathmuncher.REINFORCING_DATA[runeScale];
  3044. if (runeMatch) {
  3045. itemData.system.runes.reinforcing = runeMatch.value;
  3046. itemData.system.hp.value += runeMatch.hp;
  3047. }
  3048. } else {
  3049. const runeScale = rune.split("(").pop().split(")").shift().trim();
  3050. const runeLevel = Pathmuncher.RUNE_SCALE.indexOf(runeScale);
  3051. const runeType = rune.split("(").shift().toLowerCase().trim();
  3052. if (runeLevel !== -1) {
  3053. itemData.system.runes[runeType] = runeLevel;
  3054. }
  3055. }
  3056. });
  3057. } else if (hasProperty(itemData, "system.runes.potency")) {
  3058. itemData.system.runes.potency = parsedItem.pot;
  3059. if (type === "weapon") {
  3060. const striking = Pathmuncher.POTENCY_SCALE.indexOf(parsedItem.str);
  3061. if (striking !== -1) itemData.system.runes.striking = striking;
  3062. } else if (type === "armor") {
  3063. const resilient = Pathmuncher.RESILIENT_SCALE.indexOf(parsedItem.res);
  3064. if (resilient !== -1) itemData.system.runes.resilient = resilient;
  3065. }
  3066. }
  3067. if (type === "armor" && parsedItem.worn
  3068. && ((Number.isInteger(parsedItem.pot) && parsedItem.pot > 0)
  3069. || (parsedItem.res && parsedItem.res !== "")
  3070. )
  3071. ) {
  3072. itemData.system.equipped.invested = true;
  3073. }
  3074. if (hasProperty(itemData, "system.runes.property")) {
  3075. parsedItem.runes.forEach((property) => {
  3076. const resistantRegex = /Energy Resistant - (.*)/i;
  3077. const resistantMatch = property.match(resistantRegex);
  3078. const rune = resistantMatch
  3079. ? `${resistantMatch[1]} Resistant`
  3080. : property;
  3081. itemData.system.runes.property.push(Seasoning.slugD(rune));
  3082. });
  3083. }
  3084. if (parsedItem.mat) {
  3085. const material = parsedItem.mat.split(" (")[0];
  3086. itemData.system.material.type = Seasoning.slugD(material);
  3087. itemData.system.material.grade = Seasoning.getMaterialGrade(parsedItem.mat);
  3088. }
  3089. }
  3090. async #createWeaponItem(data) {
  3091. // { pbName, name, prof, qty, die, display, increasedDice, pot, str, mat, runes, attack, damageBonus, extraDamage, damageType }
  3092. src_logger.debug("Generating weapon for", data);
  3093. const indexMatch = this.compendiumMatchers["equipment"].getMatch(data.pbName, data.foundryName);
  3094. if (!indexMatch) {
  3095. src_logger.error(`Unable to match weapon item ${data.name}`, data);
  3096. this.bad.push({ pbName: data.pbName, type: "weapon", details: { w: data } });
  3097. return null;
  3098. }
  3099. const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  3100. const itemData = doc.toObject();
  3101. itemData._id = foundry.utils.randomID();
  3102. itemData.system.quantity = data.qty;
  3103. // because some shields don't have damage dice, but come in as weapons on pathbuilder
  3104. if (itemData.type === "weapon") {
  3105. if (data.die) itemData.system.damage.die = data.die;
  3106. Pathmuncher.applyRunes(data, itemData, "weapon");
  3107. }
  3108. if (data.display.startsWith("Large ") || data.increasedDice) {
  3109. itemData.system.size = "lg";
  3110. } else if (data.display && !Seasoning.IGNORED_EQUIPMENT_DISPLAY(data.display)) {
  3111. itemData.name = data.display;
  3112. }
  3113. this.#resizeItem(itemData);
  3114. this.result.weapons.push(itemData);
  3115. data.added = true;
  3116. data.addedId = itemData._id;
  3117. return itemData;
  3118. }
  3119. async #generateWeaponItems() {
  3120. for (const w of this.parsed.weapons) {
  3121. if (Seasoning.IGNORED_EQUIPMENT().includes(w.pbName)) {
  3122. w.added = true;
  3123. w.addedAutoId = "ignored";
  3124. continue;
  3125. }
  3126. await this.#createWeaponItem(w);
  3127. }
  3128. }
  3129. #adjustArmorItem(itemData, parsedArmor) {
  3130. itemData._id = foundry.utils.randomID();
  3131. itemData.system.equipped.value = parsedArmor.worn ?? false;
  3132. if (!Seasoning.RESTRICTED_EQUIPMENT().some((i) => itemData.name.startsWith(i))) {
  3133. itemData.system.equipped.inSlot = parsedArmor.worn ?? false;
  3134. itemData.system.quantity = parsedArmor.qty;
  3135. const isShield = parsedArmor.prof === "shield";
  3136. itemData.system.equipped.handsHeld = isShield && parsedArmor.worn ? 1 : 0;
  3137. itemData.system.equipped.carryType = isShield && parsedArmor.worn ? "held" : "worn";
  3138. Pathmuncher.applyRunes(parsedArmor, itemData, "armor");
  3139. }
  3140. if (parsedArmor.display) itemData.name = parsedArmor.display;
  3141. this.#resizeItem(itemData);
  3142. return itemData;
  3143. }
  3144. async #generateArmorItems() {
  3145. for (const a of this.parsed.armor) {
  3146. if (Seasoning.IGNORED_EQUIPMENT().includes(a.pbName)) {
  3147. a.added = true;
  3148. a.addedAutoId = "ignored";
  3149. continue;
  3150. }
  3151. src_logger.debug("Generating armor for", a);
  3152. if (Seasoning.GRANTED_ITEMS_LIST().includes(a.pbName)) {
  3153. const existingItem = this.result.armor.find((i) => i.name === a.foundryName);
  3154. if (existingItem) {
  3155. a.added = true;
  3156. a.addedId = existingItem._id;
  3157. src_logger.debug(`Ignoring armor item ${a.pbName} as it has been granted by a feature`);
  3158. continue;
  3159. }
  3160. }
  3161. const indexMatch = this.compendiumMatchers["equipment"].getMatch(a.foundryName, `${a.pbName} Armor`);
  3162. if (!indexMatch) {
  3163. src_logger.error(`Unable to match armor kit item ${a.name}`, a);
  3164. this.bad.push({ pbName: a.pbName, type: "armor", details: { a } });
  3165. continue;
  3166. }
  3167. const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  3168. const itemData = this.#adjustArmorItem(doc.toObject(), a);
  3169. this.result.armor.push(itemData);
  3170. a.addedId = itemData._id;
  3171. a.added = true;
  3172. }
  3173. }
  3174. getClassSpellCastingType(dual = false) {
  3175. const classCaster = dual
  3176. ? this.source.spellCasters.find((caster) => caster.name === this.source.dualClass)
  3177. : this.source.spellCasters.find((caster) => caster.name === this.source.class);
  3178. const type = classCaster?.spellcastingType;
  3179. if (type || this.source.spellCasters.length === 0) return type ?? "spontaneous";
  3180. // if no type and multiple spell casters, then return the first spell casting type
  3181. return this.source.spellCasters[0].spellcastingType ?? "spontaneous";
  3182. }
  3183. // aims to determine the class magic tradition for a spellcasting block
  3184. getClassMagicTradition(caster) {
  3185. const classCaster = [this.source.class, this.source.dualClass].includes(caster.name);
  3186. const tradition = classCaster ? caster?.magicTradition : undefined;
  3187. // if a caster tradition or no spellcasters, return divine
  3188. if (tradition || this.source.spellCasters.length === 0) return tradition ?? "divine";
  3189. // not a focus traditions
  3190. if (caster.magicTradition !== "focus" && ["divine", "occult", "primal", "arcane"].includes(caster.magicTradition)) {
  3191. return caster.magicTradition;
  3192. }
  3193. // this spell caster type is not a class, determine class tradition based on ability
  3194. const abilityTradition = this.source.spellCasters.find((c) =>
  3195. [this.source.class, this.source.dualClass].includes(c.name)
  3196. && c.ability === caster.ability
  3197. );
  3198. if (abilityTradition) return abilityTradition.magicTradition;
  3199. // if no type and multiple spell casters, then return the first spell casting type
  3200. return this.source.spellCasters[0].magicTradition && this.source.spellCasters[0].magicTradition !== "focus"
  3201. ? this.source.spellCasters[0].magicTradition
  3202. : "divine";
  3203. }
  3204. #applySpellBlending(spellcastingEntity, caster) {
  3205. if (caster.blendedSpells.length === 0) return;
  3206. const remove = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
  3207. const add = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
  3208. // find adjustments
  3209. caster.blendedSpells.forEach((slot) => {
  3210. remove[slot.levelFrom]++;
  3211. add[slot.LevelTo]++;
  3212. });
  3213. for (let i = 0; i <= 10; i++) {
  3214. const toAdd = this.options.adjustBlendedSlots ? 0 : Math.floor(add[i] / 2);
  3215. const toRemove = this.options.adjustBlendedSlots ? remove[i] : 0;
  3216. const adjustment = 0 - toRemove - toAdd;
  3217. src_logger.debug("Adjusting spells for spell blending", { i, adjustment, add, remove, toAdd, max: spellcastingEntity.slots[`slot${i}`].max });
  3218. spellcastingEntity.slots[`slot${i}`].max += adjustment;
  3219. spellcastingEntity.slots[`slot${i}`].value += adjustment;
  3220. }
  3221. }
  3222. #generateSpellCaster(caster) {
  3223. const isFocus = caster.magicTradition === "focus";
  3224. const magicTradition = this.getClassMagicTradition(caster);
  3225. const spellcastingType = isFocus ? "focus" : caster.spellcastingType;
  3226. const flexible = false; // placeholder
  3227. const name = isFocus ? `${src_utils.capitalize(magicTradition)} ${caster.name}` : caster.name;
  3228. const spellcastingEntity = {
  3229. ability: {
  3230. value: caster.ability,
  3231. },
  3232. proficiency: {
  3233. value: caster.proficiency / 2,
  3234. },
  3235. spelldc: {
  3236. item: 0,
  3237. },
  3238. tradition: {
  3239. value: magicTradition,
  3240. },
  3241. prepared: {
  3242. value: spellcastingType,
  3243. flexible,
  3244. },
  3245. slots: {},
  3246. showUnpreparedSpells: { value: true },
  3247. showSlotlessLevels: { value: true },
  3248. };
  3249. // apply slot data
  3250. for (let i = 0; i <= 10; i++) {
  3251. spellcastingEntity.slots[`slot${i}`] = {
  3252. max: caster.perDay[i],
  3253. prepared: {},
  3254. value: caster.perDay[i],
  3255. };
  3256. }
  3257. // adjust slots for spell blended effects
  3258. this.#applySpellBlending(spellcastingEntity, caster);
  3259. const data = {
  3260. _id: foundry.utils.randomID(),
  3261. name,
  3262. type: "spellcastingEntry",
  3263. system: spellcastingEntity,
  3264. };
  3265. this.result.casters.push(data);
  3266. return data;
  3267. }
  3268. #generateFocusSpellCaster(proficiency, ability, tradition) {
  3269. const data = {
  3270. _id: foundry.utils.randomID(),
  3271. name: `${src_utils.capitalize(tradition)} Focus Tradition`,
  3272. type: "spellcastingEntry",
  3273. system: {
  3274. ability: {
  3275. value: ability,
  3276. },
  3277. proficiency: {
  3278. value: proficiency / 2,
  3279. },
  3280. spelldc: {
  3281. item: 0,
  3282. },
  3283. tradition: {
  3284. value: tradition,
  3285. },
  3286. prepared: {
  3287. value: "focus",
  3288. flexible: false,
  3289. },
  3290. showUnpreparedSpells: { value: true },
  3291. },
  3292. };
  3293. this.result.casters.push(data);
  3294. return data;
  3295. }
  3296. async #loadSpell(spell, casterId, debugData) {
  3297. const spellName = spellRename(spell.split("(")[0].trim());
  3298. src_logger.debug("focus spell details", { spell, spellName, debugData });
  3299. const indexMatch = this.compendiumMatchers["spells"].getMatch(spell, spellName, true);
  3300. if (!indexMatch) {
  3301. if (debugData.psychicAmpSpell) return undefined;
  3302. src_logger.error(`Unable to match focus spell ${spell}`, { spell, spellName, debugData });
  3303. this.bad.push({ pbName: spell, type: "spell", details: { originalName: spell, name: spellName, debugData } });
  3304. return undefined;
  3305. }
  3306. const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  3307. const itemData = doc.toObject();
  3308. itemData._id = foundry.utils.randomID();
  3309. itemData.system.location.value = casterId;
  3310. return itemData;
  3311. }
  3312. // eslint-disable-next-line complexity
  3313. async #processCasterSpells(instance, caster, spellEnhancements, forcePrepare = false) {
  3314. const spellNames = {};
  3315. for (const spellSelection of caster.spells) {
  3316. const level = spellSelection.spellLevel;
  3317. const preparedAtLevel = caster.prepared?.length > 0
  3318. ? (caster.prepared.find((p) => p.spellLevel === level)?.list ?? [])
  3319. : [];
  3320. let preparedValue = 0;
  3321. // const preparedMap = preparedAtLevel.reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map());
  3322. for (const [i, spell] of spellSelection.list.entries()) {
  3323. src_logger.debug(`Checking spell at ${i} for level ${level}`, { spell });
  3324. const itemData = await this.#loadSpell(spell, instance._id, {
  3325. spellSelection,
  3326. list: spellSelection.list,
  3327. level,
  3328. instance,
  3329. });
  3330. if (itemData) {
  3331. itemData.system.location.heightenedLevel = level;
  3332. spellNames[spell] = itemData._id;
  3333. this.result.spells.push(itemData);
  3334. // if the caster is prepared we don't prepare spells as all known spells come through in JSON
  3335. if (instance.system.prepared.value !== "prepared"
  3336. || spellEnhancements?.preparePBSpells
  3337. || forcePrepare
  3338. || (caster.spellcastingType === "prepared"
  3339. && preparedAtLevel.length === 0 && spellSelection.list.length <= caster.perDay[level])
  3340. ) {
  3341. src_logger.debug(`Preparing spell ${itemData.name} for level ${level}`, { spell });
  3342. // eslint-disable-next-line require-atomic-updates
  3343. instance.system.slots[`slot${level}`].prepared[preparedValue] = { id: itemData._id };
  3344. preparedValue++;
  3345. }
  3346. }
  3347. }
  3348. for (const spell of preparedAtLevel) {
  3349. // if (spellNames.includes(spellName)) continue;
  3350. const parsedSpell = getProperty(spellNames, spell);
  3351. const itemData = parsedSpell
  3352. ? this.result.spells.find((s) => s._id === parsedSpell)
  3353. : await this.#loadSpell(spell, instance._id, {
  3354. spellSelection,
  3355. level,
  3356. instance,
  3357. });
  3358. if (itemData) {
  3359. itemData.system.location.heightenedLevel = level;
  3360. if (itemData && !parsedSpell) {
  3361. spellNames[spell] = itemData._id;
  3362. this.result.spells.push(itemData);
  3363. }
  3364. src_logger.debug(`Preparing spell ${itemData.name} for level ${level}`, { spellName: spell });
  3365. // eslint-disable-next-line require-atomic-updates
  3366. instance.system.slots[`slot${level}`].prepared[preparedValue] = { id: itemData._id };
  3367. preparedValue++;
  3368. } else {
  3369. src_logger.warn(`Unable to find spell ${spell}`);
  3370. }
  3371. }
  3372. if (spellEnhancements?.knownSpells) {
  3373. for (const spell of spellEnhancements.knownSpells) {
  3374. const itemData = await this.#loadSpell(spell, instance._id, {
  3375. spellEnhancements,
  3376. instance,
  3377. });
  3378. if (itemData && !hasProperty(spellNames, itemData.name)) {
  3379. itemData.system.location.heightenedLevel = level;
  3380. spellNames[spell] = itemData._id;
  3381. this.result.spells.push(itemData);
  3382. }
  3383. }
  3384. }
  3385. }
  3386. }
  3387. async #processFocusSpells(instance, spells) {
  3388. for (const spell of spells) {
  3389. const itemData = await this.#loadSpell(spell, instance._id, {
  3390. instance,
  3391. spells,
  3392. spell,
  3393. });
  3394. if (itemData) this.result.spells.push(itemData);
  3395. if (spell.endsWith("(Amped)")) {
  3396. const psychicSpell = spell.replace("(Amped)", "(Psychic)");
  3397. const psychicItemData = await this.#loadSpell(psychicSpell, instance._id, {
  3398. instance,
  3399. spells,
  3400. spell: psychicSpell,
  3401. psychicAmpSpell: true,
  3402. });
  3403. if (psychicItemData) {
  3404. this.result.spells.push(psychicItemData);
  3405. }
  3406. }
  3407. }
  3408. }
  3409. async #processRituals() {
  3410. if (!this.source.rituals) return;
  3411. const ritualCompendium = new CompendiumMatcher({
  3412. type: "spells",
  3413. indexFields: ["name", "type", "system.slug", "system.ritual"],
  3414. });
  3415. await ritualCompendium.loadCompendiums();
  3416. for (const ritual of this.source.rituals) {
  3417. const ritualName = ritual.split("(")[0].trim();
  3418. src_logger.debug("focus spell details", { ritual, spellName: ritualName });
  3419. const indexMatch = this.compendiumMatchers["spells"].getNameMatchWithFilter(ritualName, ritualName);
  3420. if (!indexMatch || !hasProperty(indexMatch, "system.ritual")) {
  3421. src_logger.error(`Unable to match ritual spell ${ritual}`, { spell: ritual, spellName: ritualName });
  3422. this.bad.push({ pbName: ritual, type: "spell", details: { originalName: ritual, name: ritualName } });
  3423. continue;
  3424. }
  3425. const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  3426. const itemData = doc.toObject();
  3427. itemData._id = foundry.utils.randomID();
  3428. this.result.spells.push(itemData);
  3429. }
  3430. }
  3431. async #processSpells() {
  3432. for (const caster of this.source.spellCasters) {
  3433. src_logger.debug("Generating caster for", caster);
  3434. if (Number.isInteger(parseInt(caster.focusPoints))) this.result.focusPool += caster.focusPoints;
  3435. const instance = this.#generateSpellCaster(caster);
  3436. src_logger.debug("Generated caster instance", instance);
  3437. const spellEnhancements = Seasoning.getSpellCastingFeatureAdjustment(caster.name);
  3438. let forcePrepare = false;
  3439. if (hasProperty(spellEnhancements, "showSlotless")) {
  3440. instance.system.showSlotlessLevels.value = getProperty(spellEnhancements, "showSlotless");
  3441. } else if (
  3442. caster.spellcastingType === "prepared"
  3443. && ![this.source.class, this.source.dualClass].includes(caster.name)
  3444. ) {
  3445. const slotToPreparedMatch = caster.spells.every((spellBlock) => {
  3446. const spellCount = spellBlock.list.length;
  3447. const perDay = caster.perDay[spellBlock.spellLevel];
  3448. return perDay === spellCount;
  3449. });
  3450. src_logger.debug(`Setting ${caster.name} show all slots to ${!slotToPreparedMatch}`);
  3451. instance.system.showSlotlessLevels.value = !slotToPreparedMatch;
  3452. forcePrepare = slotToPreparedMatch;
  3453. }
  3454. await this.#processCasterSpells(instance, caster, spellEnhancements, forcePrepare);
  3455. }
  3456. for (const tradition of ["occult", "primal", "divine", "arcane"]) {
  3457. const traditionData = getProperty(this.source, `focus.${tradition}`);
  3458. src_logger.debug(`Checking for focus tradition ${tradition}`);
  3459. if (!traditionData) continue;
  3460. for (const ability of ["str", "dex", "con", "int", "wis", "cha"]) {
  3461. const abilityData = getProperty(traditionData, ability);
  3462. src_logger.debug(`Checking for focus tradition ${tradition} with ability ${ability}`);
  3463. if (!abilityData) continue;
  3464. src_logger.debug("Generating focus spellcasting ", { tradition, traditionData, ability });
  3465. const instance = this.#generateFocusSpellCaster(abilityData.proficiency, ability, tradition);
  3466. if (abilityData.focusCantrips && abilityData.focusCantrips.length > 0) {
  3467. await this.#processFocusSpells(instance, abilityData.focusCantrips);
  3468. }
  3469. if (abilityData.focusSpells && abilityData.focusSpells.length > 0) {
  3470. await this.#processFocusSpells(instance, abilityData.focusSpells);
  3471. }
  3472. }
  3473. }
  3474. setProperty(this.result.character, "system.resources.focus.max", this.source.focusPoints);
  3475. setProperty(this.result.character, "system.resources.focus.value", this.source.focusPoints);
  3476. }
  3477. async #generateLores() {
  3478. for (const lore of this.source.lores) {
  3479. const data = {
  3480. name: lore[0],
  3481. type: "lore",
  3482. system: {
  3483. proficient: {
  3484. value: lore[1] / 2,
  3485. },
  3486. featType: "",
  3487. mod: {
  3488. value: 0,
  3489. },
  3490. item: {
  3491. value: 0,
  3492. },
  3493. },
  3494. };
  3495. this.result.lores.push(data);
  3496. }
  3497. }
  3498. async #generateMoney() {
  3499. const compendium = game.packs.get("pf2e.equipment-srd");
  3500. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  3501. const moneyLookup = [
  3502. { slug: "platinum-pieces", type: "pp" },
  3503. { slug: "gold-pieces", type: "gp" },
  3504. { slug: "silver-pieces", type: "sp" },
  3505. { slug: "copper-pieces", type: "cp" },
  3506. ];
  3507. for (const lookup of moneyLookup) {
  3508. const indexMatch = index.find((i) => i.system.slug === lookup.slug);
  3509. if (indexMatch) {
  3510. const doc = await compendium.getDocument(indexMatch._id);
  3511. const itemData = doc.toObject();
  3512. itemData._id = foundry.utils.randomID();
  3513. itemData.system.quantity = this.source.money[lookup.type];
  3514. this.result.money.push(itemData);
  3515. }
  3516. }
  3517. }
  3518. async #processFormulas() {
  3519. const uuids = [];
  3520. for (const formulaSource of this.source.formula) {
  3521. for (const formulaName of formulaSource.known) {
  3522. const indexMatch = this.compendiumMatchers["formulas"].getMatch(formulaName, formulaName);
  3523. if (!indexMatch) {
  3524. src_logger.error(`Unable to match formula ${formulaName}`, { formulaSource, name: formulaName });
  3525. this.bad.push({ pbName: formulaName, type: "formula", details: { formulaSource, name: formulaName } });
  3526. continue;
  3527. }
  3528. const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
  3529. uuids.push({ uuid: doc.uuid });
  3530. }
  3531. }
  3532. setProperty(this.result.character, "system.crafting.formulas", uuids);
  3533. }
  3534. async #processFeats() {
  3535. this.#sortParsedFeats();
  3536. // pre pass for standard items
  3537. for (let i = 1; i <= this.result.character.system.details.level.value; i++) {
  3538. await this.#generateFeatItems("feats", { typeFilter: "Ancestry Feat", levelCap: i, excludeChild: true, excludeParents: true });
  3539. await this.#generateFeatItems("feats", { typeFilter: "Skill Feat", levelCap: i, excludeChild: true, excludeParents: true });
  3540. await this.#generateFeatItems("feats", { typeFilter: "Class Feat", levelCap: i, excludeChild: true, excludeParents: true });
  3541. await this.#generateFeatItems("feats", { typeFilter: "General Feat", levelCap: i, excludeChild: true, excludeParents: true });
  3542. }
  3543. await this.#generateFeatItems("ancestryFeatures", { excludeChild: true, excludeParents: true });
  3544. // prepass for non-child items
  3545. for (let i = 1; i <= this.result.character.system.details.level.value; i++) {
  3546. await this.#generateFeatItems("feats", { typeFilter: "Ancestry Feat", levelCap: i, excludeChild: true });
  3547. await this.#generateFeatItems("feats", { typeFilter: "Skill Feat", levelCap: i, excludeChild: true });
  3548. await this.#generateFeatItems("feats", { typeFilter: "Class Feat", levelCap: i, excludeChild: true });
  3549. await this.#generateFeatItems("feats", { typeFilter: "General Feat", levelCap: i, excludeChild: true });
  3550. await this.#generateFeatItems("feats", { typeFilter: "Archetype Feat", levelCap: i, excludeChild: true });
  3551. }
  3552. await this.#generateFeatItems("ancestryFeatures", { excludeChild: true });
  3553. this.#setSkills();
  3554. // final pass, include all
  3555. this.#statusUpdate(1, 5, "Feats");
  3556. await this.#generateFeatItems("feats");
  3557. this.#statusUpdate(2, 5, "Feats");
  3558. await this.#generateFeatItems("ancestryFeatures");
  3559. this.#statusUpdate(3, 5, "Feats");
  3560. await this.#generateSpecialItems("ancestryFeatures");
  3561. this.#statusUpdate(4, 5, "Feats");
  3562. await this.#generateSpecialItems("classFeatures");
  3563. this.#statusUpdate(5, 5, "Feats");
  3564. await this.#generateSpecialItems("actions");
  3565. }
  3566. async #processEquipment() {
  3567. this.#statusUpdate(1, 4, "Equipment");
  3568. await this.#processEquipmentItems();
  3569. this.#statusUpdate(2, 4, "Weapons");
  3570. await this.#generateWeaponItems();
  3571. this.#statusUpdate(3, 4, "Armor");
  3572. await this.#generateArmorItems();
  3573. this.#statusUpdate(2, 4, "Money");
  3574. await this.#generateMoney();
  3575. }
  3576. async #generateTempActor(documents = [], includePassedDocumentsRules = false, includeGrants = false, includeFlagsOnly = false) {
  3577. const actorData = mergeObject({ type: "character" }, this.result.character);
  3578. actorData.name = `Mr Temp (${this.result.character.name})`;
  3579. if (documents.map((d) => d.name.split("(")[0].trim().toLowerCase()).includes("skill training")) {
  3580. delete actorData.system.skills;
  3581. }
  3582. const actor = await Actor.create(actorData, { renderSheet: false });
  3583. const currentState = duplicate(this.result);
  3584. // console.warn("Initial temp actor", deepClone(actor));
  3585. const currentItems = [
  3586. ...currentState.deity,
  3587. ...currentState.ancestry,
  3588. ...currentState.heritage,
  3589. ...currentState.background,
  3590. ...currentState.class,
  3591. ...currentState.lores,
  3592. ...currentState.feats,
  3593. ...currentState.casters,
  3594. // ...currentState.spells,
  3595. // ...currentState.equipment,
  3596. // ...currentState.weapons,
  3597. // ...currentState.armor,
  3598. // ...currentState.treasure,
  3599. // ...currentState.money,
  3600. ];
  3601. for (const doc of documents) {
  3602. if (!currentItems.some((d) => d._id === doc._id)) {
  3603. currentItems.push(deepClone(doc));
  3604. }
  3605. }
  3606. try {
  3607. // if the rule selected is an object, id doesn't take on import
  3608. const ruleUpdates = [];
  3609. for (const i of deepClone(currentItems)) {
  3610. if (!i.system.rules || i.system.rules.length === 0) continue;
  3611. const isPassedDocument = documents.some((d) => d._id === i._id);
  3612. if (isPassedDocument && !includePassedDocumentsRules && !includeFlagsOnly) continue;
  3613. const objectSelectionRules = i.system.rules
  3614. .filter((r) => {
  3615. const evaluateRules = ["RollOption", "ChoiceSet"].includes(r.key) && r.selection;
  3616. return !includeFlagsOnly || evaluateRules; // && ["RollOption", "GrantItem", "ChoiceSet", "ActiveEffectLike"].includes(r.key);
  3617. // || (["ChoiceSet"].includes(r.key) && r.selection);
  3618. })
  3619. .map((r) => {
  3620. r.ignored = false;
  3621. return r;
  3622. });
  3623. if (objectSelectionRules.length > 0) {
  3624. ruleUpdates.push({
  3625. _id: i._id,
  3626. system: {
  3627. rules: objectSelectionRules,
  3628. },
  3629. });
  3630. }
  3631. }
  3632. // console.warn("Rule updates", duplicate(ruleUpdates));
  3633. const items = duplicate(currentItems).map((i) => {
  3634. if (i.system.items) i.system.items = [];
  3635. if (i.system.rules) {
  3636. i.system.rules = i.system.rules
  3637. .filter((r) => {
  3638. const isPassedDocument = documents.some((d) => d._id === i._id);
  3639. const isChoiceSetSelection = ["ChoiceSet"].includes(r.key) && r.selection;
  3640. // const choiceSetSelectionObject = isChoiceSetSelection && utils.isObject(r.selection);
  3641. const choiceSetSelectionNotObject = isChoiceSetSelection && !src_utils.isObject(r.selection);
  3642. // const grantRuleWithFlag = includeGrants && ["GrantItem"].includes(r.key) && r.flag;
  3643. const grantRuleWithoutFlag = includeGrants && ["GrantItem"].includes(r.key) && !r.flag;
  3644. // const genericDiscardRule = ["ChoiceSet", "GrantItem", "ActiveEffectLike", "Resistance", "Strike", "AdjustModifier"].includes(r.key);
  3645. const genericDiscardRule = ["ChoiceSet", "GrantItem"].includes(r.key);
  3646. const grantRuleFromItemFlag
  3647. = includeGrants && ["GrantItem"].includes(r.key) && r.uuid.startsWith("{item|flags");
  3648. const rollOptionsRule = ["RollOption"].includes(r.key);
  3649. const notPassedDocumentRules
  3650. = !isPassedDocument
  3651. && (choiceSetSelectionNotObject
  3652. // || grantRuleWithFlag
  3653. || grantRuleWithoutFlag
  3654. || !genericDiscardRule);
  3655. const passedDocumentRules
  3656. = isPassedDocument
  3657. && includePassedDocumentsRules
  3658. && (isChoiceSetSelection || grantRuleWithoutFlag || grantRuleFromItemFlag || rollOptionsRule);
  3659. return notPassedDocumentRules || passedDocumentRules;
  3660. })
  3661. .map((r) => {
  3662. // if choices is a string or an object then we replace with the query string results
  3663. if ((src_utils.isString(r.choices) || src_utils.isObject(r.choices)) && r.choiceQueryResults) {
  3664. r.choices = r.choiceQueryResults;
  3665. }
  3666. r.ignored = false;
  3667. return r;
  3668. });
  3669. }
  3670. return i;
  3671. });
  3672. // const items2 = duplicate(currentItems).map((i) => {
  3673. // if (i.system.items) i.system.items = [];
  3674. // if (i.system.rules) i.system.rules = i.system.rules.filter((r) =>
  3675. // (!documents.some((d) => d._id === i._id)
  3676. // && ((["ChoiceSet",].includes(r.key) && r.selection)
  3677. // // || (["GrantItem"].includes(r.key) && r.flag)
  3678. // || !["ChoiceSet", "GrantItem"].includes(r.key)
  3679. // ))
  3680. // || (includePassedDocumentsRules && documents.some((d) => d._id === i._id) && ["ChoiceSet",].includes(r.key) && r.selection)
  3681. // ).map((r) => {
  3682. // if ((typeof r.choices === 'string' || r.choices instanceof String)
  3683. // || (typeof r.choices === 'object' && !Array.isArray(r.choices) && r.choices !== null && r.choiceQueryResults)
  3684. // ) {
  3685. // r.choices = r.choiceQueryResults;
  3686. // }
  3687. // r.ignored = false;
  3688. // return r;
  3689. // });
  3690. // return i;
  3691. // });
  3692. // console.warn("temp items", {
  3693. // documents: deepClone(currentItems),
  3694. // items: deepClone(items),
  3695. // // items2: deepClone(items2),
  3696. // // diff: diffObject(items, items2),
  3697. // includePassedDocumentsRules,
  3698. // includeGrants,
  3699. // });
  3700. await actor.createEmbeddedDocuments("Item", items, { keepId: true });
  3701. // console.warn("restoring selection rules to temp items", ruleUpdates);
  3702. await actor.updateEmbeddedDocuments("Item", ruleUpdates);
  3703. const itemUpdates = [];
  3704. for (const [key, value] of Object.entries(this.autoAddedFeatureItems)) {
  3705. itemUpdates.push({
  3706. _id: `${key}`,
  3707. system: {
  3708. items: deepClone(value),
  3709. },
  3710. });
  3711. }
  3712. // console.warn("restoring feature items to temp items", itemUpdates);
  3713. await actor.updateEmbeddedDocuments("Item", itemUpdates);
  3714. src_logger.debug("Final temp actor", actor);
  3715. } catch (err) {
  3716. src_logger.error("Temp actor creation failed", {
  3717. actor,
  3718. documents,
  3719. thisData: deepClone(this.result),
  3720. actorData,
  3721. err,
  3722. currentItems,
  3723. this: this,
  3724. });
  3725. }
  3726. return actor;
  3727. }
  3728. async processCharacter() {
  3729. if (!this.source) return;
  3730. await this.#prepare();
  3731. this.#statusUpdate(1, 12, "Character");
  3732. await this.#processCore();
  3733. this.#statusUpdate(2, 12, "Formula");
  3734. await this.#processFormulas();
  3735. this.#statusUpdate(3, 12, "Deity");
  3736. await this.#processGenericCompendiumLookup("deities", this.source.deity, "deity");
  3737. this.#statusUpdate(4, 12, "Ancestry");
  3738. await this.#processGenericCompendiumLookup("ancestries", this.source.ancestry, "ancestry");
  3739. this.#statusUpdate(5, 12, "Heritage");
  3740. await this.#processGenericCompendiumLookup("heritages", this.source.heritage, "heritage");
  3741. this.#statusUpdate(6, 12, "Background");
  3742. await this.#processGenericCompendiumLookup("backgrounds", this.source.background, "background");
  3743. this.#setSkills(true);
  3744. this.#statusUpdate(7, 12, "Class");
  3745. await this.#processGenericCompendiumLookup("classes", this.source.class, "class");
  3746. this.#setAbilityBoosts();
  3747. this.#statusUpdate(8, 12, "FeatureRec");
  3748. await this.#processFeats();
  3749. this.#statusUpdate(10, 12, "Equipment");
  3750. await this.#processEquipment();
  3751. this.#statusUpdate(11, 12, "Spells");
  3752. await this.#processSpells();
  3753. this.#statusUpdate(11, 12, "Rituals");
  3754. await this.#processRituals();
  3755. this.#statusUpdate(12, 12, "Lores");
  3756. await this.#generateLores();
  3757. }
  3758. async #removeDocumentsToBeUpdated() {
  3759. const moneyIds = this.actor.items.filter((i) =>
  3760. i.type === "treasure"
  3761. && ["Platinum Pieces", "Gold Pieces", "Silver Pieces", "Copper Pieces"].includes(i.name)
  3762. );
  3763. const classIds = this.actor.items.filter((i) => i.type === "class").map((i) => i._id);
  3764. const deityIds = this.actor.items.filter((i) => i.type === "deity").map((i) => i._id);
  3765. const backgroundIds = this.actor.items.filter((i) => i.type === "background").map((i) => i._id);
  3766. const heritageIds = this.actor.items.filter((i) => i.type === "heritage").map((i) => i._id);
  3767. const ancestryIds = this.actor.items.filter((i) => i.type === "ancestry").map((i) => i._id);
  3768. const treasureIds = this.actor.items
  3769. .filter((i) => i.type === "treasure" && !moneyIds.includes(i.id))
  3770. .map((i) => i._id);
  3771. const featIds = this.actor.items.filter((i) => i.type === "feat").map((i) => i._id);
  3772. const actionIds = this.actor.items.filter((i) => i.type === "action").map((i) => i._id);
  3773. const equipmentIds = this.actor.items
  3774. .filter((i) => i.type === "equipment" || i.type === "backpack" || i.type === "consumable")
  3775. .map((i) => i._id);
  3776. const weaponIds = this.actor.items.filter((i) => i.type === "weapon").map((i) => i._id);
  3777. const armorIds = this.actor.items.filter((i) => i.type === "armor").map((i) => i._id);
  3778. const loreIds = this.actor.items.filter((i) => i.type === "lore").map((i) => i._id);
  3779. const spellIds = this.actor.items
  3780. .filter((i) => i.type === "spell" || i.type === "spellcastingEntry")
  3781. .map((i) => i._id);
  3782. const formulaIds = this.actor.system.formulas;
  3783. src_logger.debug("ids", {
  3784. moneyIds,
  3785. deityIds,
  3786. classIds,
  3787. backgroundIds,
  3788. heritageIds,
  3789. ancestryIds,
  3790. treasureIds,
  3791. featIds,
  3792. actionIds,
  3793. equipmentIds,
  3794. weaponIds,
  3795. armorIds,
  3796. loreIds,
  3797. spellIds,
  3798. formulaIds,
  3799. });
  3800. // eslint-disable-next-line complexity
  3801. const keepIds = this.actor.items.filter((i) =>
  3802. (!this.options.addMoney && moneyIds.includes(i._id))
  3803. || (!this.options.addClass && classIds.includes(i._id))
  3804. || (!this.options.addDeity && deityIds.includes(i._id))
  3805. || (!this.options.addBackground && backgroundIds.includes(i._id))
  3806. || (!this.options.addHeritage && heritageIds.includes(i._id))
  3807. || (!this.options.addAncestry && ancestryIds.includes(i._id))
  3808. || (!this.options.addTreasure && treasureIds.includes(i._id))
  3809. || (!this.options.addFeats && (featIds.includes(i._id) || actionIds.includes(i._id)))
  3810. || (!this.options.addEquipment && equipmentIds.includes(i._id))
  3811. || (!this.options.addWeapons && weaponIds.includes(i._id))
  3812. || (!this.options.addArmor && armorIds.includes(i._id))
  3813. || (!this.options.addLores && loreIds.includes(i._id))
  3814. || (!this.options.addSpells && spellIds.includes(i._id))
  3815. ).map((i) => i._id);
  3816. const deleteIds = this.actor.items.filter((i) => !keepIds.includes(i._id)).map((i) => i._id);
  3817. src_logger.debug("ids", {
  3818. deleteIds,
  3819. keepIds,
  3820. });
  3821. await this.actor.deleteEmbeddedDocuments("Item", deleteIds);
  3822. }
  3823. async #createAndUpdateItemsWithRuleRestore(items) {
  3824. const ruleUpdates = [];
  3825. const newItems = deepClone(items);
  3826. for (const item of newItems) {
  3827. if (item.system.rules?.length > 0) {
  3828. ruleUpdates.push({
  3829. _id: item._id,
  3830. system: {
  3831. rules: deepClone(item.system.rules).map((r) => {
  3832. delete r.choiceQueryResults;
  3833. return r;
  3834. }),
  3835. },
  3836. });
  3837. item.system.rules = item.system.rules
  3838. .filter((r) => {
  3839. const excludedKeys = ["ActiveEffectLike", "AdjustModifier", "Resistance", "Strike"].includes(r.key);
  3840. const grantItemWithFlags = ["GrantItem"].includes(r.key) && (hasProperty(r, "flag") || getProperty(r, "pathmuncherImport"));
  3841. const objectSelection = ["ChoiceSet"].includes(r.key) && src_utils.isObject(r.selection);
  3842. return !excludedKeys && !grantItemWithFlags && !objectSelection;
  3843. })
  3844. .map((r) => {
  3845. if (r.key === "ChoiceSet") {
  3846. if ((src_utils.isString(r.choices) || src_utils.isObject(r.choices)) && r.choiceQueryResults) {
  3847. r.choices = r.choiceQueryResults;
  3848. }
  3849. }
  3850. if (r.pathmuncherImport) delete r.pathmuncherImport;
  3851. return r;
  3852. });
  3853. }
  3854. }
  3855. src_logger.debug("Creating items", newItems);
  3856. await this.actor.createEmbeddedDocuments("Item", newItems, { keepId: true });
  3857. src_logger.debug("Rule updates", ruleUpdates);
  3858. await this.actor.updateEmbeddedDocuments("Item", ruleUpdates);
  3859. }
  3860. async #updateItems(type) {
  3861. src_logger.debug(`Updating ${type}`, this.result[type]);
  3862. await this.actor.updateEmbeddedDocuments("Item", this.result[type]);
  3863. }
  3864. async #createActorEmbeddedDocuments() {
  3865. this.#statusUpdate(1, 12, "Character", "Eating");
  3866. if (this.options.addDeity) await this.#createAndUpdateItemsWithRuleRestore(this.result.deity);
  3867. if (this.options.addAncestry) await this.#createAndUpdateItemsWithRuleRestore(this.result.ancestry);
  3868. if (this.options.addHeritage) await this.#createAndUpdateItemsWithRuleRestore(this.result.heritage);
  3869. if (this.options.addBackground) await this.#createAndUpdateItemsWithRuleRestore(this.result.background);
  3870. if (this.options.addClass) await this.#createAndUpdateItemsWithRuleRestore(this.result.class);
  3871. if (this.options.addLores) await this.#createAndUpdateItemsWithRuleRestore(this.result.lores);
  3872. const featNums = this.result.feats.length;
  3873. if (this.options.addFeats) {
  3874. for (const [i, feat] of this.result.feats.entries()) {
  3875. // console.warn(`creating ${feat.name}`, feat);
  3876. this.#statusUpdate(i, featNums, "Feats", "Eating");
  3877. await this.#createAndUpdateItemsWithRuleRestore([feat]);
  3878. }
  3879. }
  3880. // if (this.options.addFeats) await this.#createAndUpdateItemsWithRuleRestore(this.result.feats);
  3881. if (this.options.addSpells) {
  3882. this.#statusUpdate(3, 12, "Spells", "Eating");
  3883. await this.#createAndUpdateItemsWithRuleRestore(this.result.casters);
  3884. await this.#createAndUpdateItemsWithRuleRestore(this.result.spells);
  3885. }
  3886. this.#statusUpdate(4, 12, "Equipment", "Eating");
  3887. if (this.options.addEquipment) {
  3888. await this.#createAndUpdateItemsWithRuleRestore(this.result.equipment);
  3889. await this.#updateItems("equipment");
  3890. }
  3891. if (this.options.addWeapons) await this.#createAndUpdateItemsWithRuleRestore(this.result.weapons);
  3892. if (this.options.addArmor) {
  3893. await this.#createAndUpdateItemsWithRuleRestore(this.result.armor);
  3894. await this.#updateItems("armor");
  3895. }
  3896. if (this.options.addTreasure) await this.#createAndUpdateItemsWithRuleRestore(this.result.treasure);
  3897. if (this.options.addMoney) await this.#createAndUpdateItemsWithRuleRestore(this.result.money);
  3898. }
  3899. async #restoreEmbeddedRuleLogic() {
  3900. const importedItems = this.actor.items.map((i) => i._id);
  3901. // Loop back over items and add rule and item progression data back in.
  3902. src_logger.debug("Restoring logic", { currentActor: duplicate(this.actor) });
  3903. const itemUpdates = [];
  3904. for (const [key, value] of Object.entries(this.autoAddedFeatureItems)) {
  3905. if (importedItems.includes(key)) {
  3906. itemUpdates.push({
  3907. _id: `${key}`,
  3908. system: {
  3909. items: deepClone(value),
  3910. },
  3911. });
  3912. }
  3913. }
  3914. this.#statusUpdate(1, 12, "Feats", "Clearing");
  3915. src_logger.debug("Restoring granted item logic", itemUpdates);
  3916. await this.actor.updateEmbeddedDocuments("Item", itemUpdates);
  3917. await this.actor.update({
  3918. "system.resources.focus": this.result.character.system.resources.focus,
  3919. });
  3920. }
  3921. async updateActor() {
  3922. await this.#removeDocumentsToBeUpdated();
  3923. if (!this.options.addName) {
  3924. delete this.result.character.name;
  3925. delete this.result.character.prototypeToken.name;
  3926. }
  3927. if (!this.options.addFormulas) {
  3928. delete this.result.character.system.formulas;
  3929. }
  3930. if (!this.boosts.custom) {
  3931. setProperty(this.result.character, `system.abilities`, null);
  3932. }
  3933. src_logger.debug("Generated result", this.result);
  3934. await this.actor.update(this.result.character);
  3935. await this.#createActorEmbeddedDocuments();
  3936. await this.#restoreEmbeddedRuleLogic();
  3937. }
  3938. async postImportCheck() {
  3939. const badClass = this.options.addClass
  3940. ? this.bad.filter((b) => b.type === "class").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Class")}: ${b.pbName}</li>`)
  3941. : [];
  3942. const badHeritage = this.options.addHeritage
  3943. ? this.bad.filter((b) => b.type === "heritage").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Heritage")}: ${b.pbName}</li>`)
  3944. : [];
  3945. const badAncestry = this.options.addAncestry
  3946. ? this.bad.filter((b) => b.type === "ancestry").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Ancestry")}: ${b.pbName}</li>`)
  3947. : [];
  3948. const badBackground = this.options.addBackground
  3949. ? this.bad.filter((b) => b.type === "background").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Background")}: ${b.pbName}</li>`)
  3950. : [];
  3951. const badDeity = this.options.addDeity
  3952. ? this.bad.filter((b) => b.type === "deity" && b.pbName !== "Not set" && b.pbName !== "").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Deity")}: ${b.pbName}</li>`)
  3953. : [];
  3954. const badFeats = this.options.addFeats
  3955. ? this.bad.filter((b) => b.type === "feat").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Feats")}: ${b.pbName}</li>`)
  3956. : [];
  3957. const badFeats2 = this.options.addFeats
  3958. ? Object.values(this.check).filter((b) =>
  3959. b.type === "feat"
  3960. && this.parsed.feats.some((f) => f.name === b.details.name && !f.added)
  3961. ).map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Feats")}: ${b.details.name}</li>`)
  3962. : [];
  3963. const badSpecials = this.options.addFeats
  3964. ? Object.values(this.check).filter((b) =>
  3965. (b.type === "special")
  3966. && this.parsed.specials.some((f) => f.name === b.details.name && !f.added)
  3967. ).map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Specials")}: ${b.details.originalName}</li>`)
  3968. : [];
  3969. const badEquipment = this.options.addEquipment
  3970. ? this.bad.filter((b) => b.type === "equipment").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Equipment")}: ${b.pbName}</li>`)
  3971. : [];
  3972. const badWeapons = this.options.addWeapons
  3973. ? this.bad.filter((b) => b.type === "weapons").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Weapons")}: ${b.pbName}</li>`)
  3974. : [];
  3975. const badArmor = this.options.addArmor
  3976. ? this.bad.filter((b) => b.type === "armor").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Armor")}: ${b.pbName}</li>`)
  3977. : [];
  3978. const badSpellcasting = this.options.addSpells
  3979. ? this.bad.filter((b) => b.type === "spellcasting").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Spellcasting")}: ${b.pbName}</li>`)
  3980. : [];
  3981. const badSpells = this.options.addSpells
  3982. ? this.bad.filter((b) => b.type === "spells").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Spells")}: ${b.pbName}</li>`)
  3983. : [];
  3984. const badFamiliars = this.options.addFamiliars
  3985. ? this.bad.filter((b) => b.type === "familiars").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Familiars")}: ${b.pbName}</li>`)
  3986. : [];
  3987. const badFormulas = this.options.addFormulas
  3988. ? this.bad.filter((b) => b.type === "formulas").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Formulas")}: ${b.pbName}</li>`)
  3989. : [];
  3990. const totalBad = [
  3991. ...badClass,
  3992. ...badAncestry,
  3993. ...badHeritage,
  3994. ...badBackground,
  3995. ...badDeity,
  3996. ...badFeats,
  3997. ...badFeats2,
  3998. ...badSpecials,
  3999. ...badEquipment,
  4000. ...badWeapons,
  4001. ...badArmor,
  4002. ...badSpellcasting,
  4003. ...badSpells,
  4004. ...badFamiliars,
  4005. ...badFormulas,
  4006. ];
  4007. let warning = "";
  4008. if (totalBad.length > 0) {
  4009. warning += `<p>${game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.MissingItemsOpen")}</p><ul>${totalBad.join("\n")}</ul><br>`;
  4010. }
  4011. src_logger.debug("Bad thing check", {
  4012. badClass,
  4013. badAncestry,
  4014. badHeritage,
  4015. badBackground,
  4016. badDeity,
  4017. badFeats,
  4018. badFeats2,
  4019. badSpecials,
  4020. badEquipment,
  4021. badWeapons,
  4022. badArmor,
  4023. badSpellcasting,
  4024. badSpells,
  4025. badFamiliars,
  4026. badFormulas,
  4027. totalBad,
  4028. count: totalBad.length,
  4029. focusPool: this.result.focusPool,
  4030. warning,
  4031. });
  4032. if (totalBad.length > 0) {
  4033. ui.notifications.warn(game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.CompletedWithNotes"));
  4034. new Dialog({
  4035. title: game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.ImportNotes"),
  4036. content: warning,
  4037. buttons: {
  4038. yes: {
  4039. icon: "<i class='fas fa-check'></i>",
  4040. label: game.i18n.localize("pathmuncher.Labels.Finished"),
  4041. },
  4042. },
  4043. default: "yes",
  4044. }).render(true);
  4045. } else {
  4046. ui.notifications.info(game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.CompletedSuccess"));
  4047. }
  4048. }
  4049. }
  4050. ;// CONCATENATED MODULE: ./src/app/PetShop.js
  4051. /* eslint-disable no-await-in-loop */
  4052. /* eslint-disable no-continue */
  4053. /**
  4054. * The PetShop class looks for familiars in a Pathmunch data set and creates/updates as appropriate.
  4055. */
  4056. class PetShop {
  4057. constructor ({ type = "familiar", parent, pathbuilderJson } = {}) {
  4058. this.parent = parent;
  4059. this.pathbuilderJson = pathbuilderJson;
  4060. this.type = type;
  4061. this.result = {
  4062. pets: [],
  4063. features: {},
  4064. };
  4065. this.bad = {};
  4066. this.folders = {};
  4067. }
  4068. async ensureFolder(type) {
  4069. const folderName = game.i18n.localize(`${constants.FLAG_NAME}.Folders.${type}`);
  4070. this.folders[type] = await src_utils.getOrCreateFolder(this.parent.folder, "Actor", folderName);
  4071. }
  4072. async #existingPetCheck(petName, type) {
  4073. const existingPet = game.actors.find((a) =>
  4074. a.type === type.toLowerCase()
  4075. && a.name === petName
  4076. && a.system.master.id === this.parent._id
  4077. );
  4078. if (existingPet) return existingPet.toObject();
  4079. const actorData = {
  4080. type: type.toLowerCase(),
  4081. name: petName,
  4082. system: {
  4083. master: {
  4084. id: this.parent._id,
  4085. ability: this.parent.system.details.keyability.value,
  4086. },
  4087. },
  4088. prototypeToken: {
  4089. name: petName,
  4090. },
  4091. folder: this.folders[type].id,
  4092. };
  4093. const actor = await Actor.create(actorData);
  4094. return actor.toObject();
  4095. }
  4096. #buildCore(petData) {
  4097. setProperty(petData, "system.attributes.value", this.parent.system.details.level.value * 5);
  4098. return petData;
  4099. }
  4100. async #generatePetFeatures(pet, json) {
  4101. const compendium = game.packs.get("pf2e.familiar-abilities");
  4102. const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
  4103. this.result.features[pet._id] = [];
  4104. this.bad[pet._id] = [];
  4105. for (const featureName of json.abilities) {
  4106. const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(featureName));
  4107. if (!indexMatch) {
  4108. src_logger.warn(`Unable to match pet feature ${featureName}`, { pet, json, name: featureName });
  4109. this.bad[pet._id].push({ pbName: featureName, type: "feature", details: { pet, json, name: featureName } });
  4110. continue;
  4111. }
  4112. const doc = (await compendium.getDocument(indexMatch._id)).toObject();
  4113. doc._id = foundry.utils.randomID();
  4114. this.result.features[pet._id].push(doc);
  4115. }
  4116. }
  4117. async buildPet(json) {
  4118. const name = json.name === json.type || !json.name.includes("(")
  4119. ? `${this.parent.name}'s ${json.type}`
  4120. : json.name.split("(")[1].split(")")[0];
  4121. const petData = await this.#existingPetCheck(name, json.type);
  4122. const pet = this.#buildCore(petData);
  4123. await this.#generatePetFeatures(pet, json);
  4124. this.result.pets.push(pet);
  4125. }
  4126. async updatePets() {
  4127. for (const petData of this.result.pets) {
  4128. const actor = game.actors.get(petData._id);
  4129. await actor.deleteEmbeddedDocuments("Item", [], { deleteAll: true });
  4130. await actor.update(petData);
  4131. await actor.createEmbeddedDocuments("Item", this.result.features[petData._id], { keepId: true });
  4132. }
  4133. }
  4134. async processPets() {
  4135. const petData = this.type === "familiar" && this.pathbuilderJson.familiars
  4136. ? this.pathbuilderJson.familiars
  4137. : this.pathbuilderJson.pets.filter((p) => this.type === p.type.toLowerCase());
  4138. await this.ensureFolder(src_utils.capitalize(this.type));
  4139. for (const petJson of petData) {
  4140. await this.buildPet(petJson);
  4141. }
  4142. await this.updatePets();
  4143. src_logger.debug("Pets", {
  4144. results: this.results,
  4145. bad: this.bad,
  4146. });
  4147. }
  4148. async addPetEffects() {
  4149. const features = [];
  4150. for (const petData of this.result.pets) {
  4151. for (const feature of this.result.features[petData._id].filter((f) => f.system.rules?.some((r) => r.key === "ActiveEffectLike"))) {
  4152. if (!this.parent.items.some((i) => i.type === "effect" && i.system.slug === feature.system.slug)) {
  4153. features.push(feature);
  4154. }
  4155. }
  4156. }
  4157. await this.parent.createEmbeddedDocuments("Item", features);
  4158. }
  4159. }
  4160. ;// CONCATENATED MODULE: ./src/app/PathmuncherImporter.js
  4161. class PathmuncherImporter extends FormApplication {
  4162. constructor(options, actor) {
  4163. super(options);
  4164. this.actor = game.actors.get(actor.id ? actor.id : actor._id);
  4165. this.backup = duplicate(this.actor);
  4166. this.mode = "number";
  4167. }
  4168. static get defaultOptions() {
  4169. const options = super.defaultOptions;
  4170. options.title = game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.PathmuncherImporter.Title`);
  4171. options.template = `${constants.PATH}/templates/pathmuncher.hbs`;
  4172. options.classes = ["pathmuncher"];
  4173. options.id = "pathmuncher";
  4174. options.width = 400;
  4175. options.closeOnSubmit = false;
  4176. options.tabs = [{ navSelector: ".tabs", contentSelector: "form", initial: "number" }];
  4177. return options;
  4178. }
  4179. /** @override */
  4180. async getData() {
  4181. const flags = src_utils.getFlags(this.actor);
  4182. return {
  4183. flags,
  4184. id: flags?.pathbuilderId ?? "",
  4185. actor: this.actor,
  4186. };
  4187. }
  4188. /** @override */
  4189. activateListeners(html) {
  4190. super.activateListeners(html);
  4191. $("#pathmuncher").css("height", "auto");
  4192. $(html)
  4193. .find('.item')
  4194. .on("click", (event) => {
  4195. if (!event.target?.dataset?.tab) return;
  4196. this.mode = event.target.dataset.tab;
  4197. });
  4198. }
  4199. static _updateProgress(total, count, type, prefixLabel = "Cooking") {
  4200. const localizedType = game.i18n.localize(`pathmuncher.Labels.${type}`);
  4201. const progressBar = document.getElementById("pathmuncher-status");
  4202. progressBar.style.width = `${Math.trunc((count / total) * 100)}%`;
  4203. progressBar.innerHTML = `<span>${game.i18n.localize(`pathmuncher.Labels.${prefixLabel}`)} (${localizedType})...</span>`;
  4204. }
  4205. async _updateObject(event, formData) {
  4206. document.getElementById("pathmuncher-button").disabled = true;
  4207. const pathbuilderId = formData.textBoxBuildID;
  4208. const options = {
  4209. pathbuilderId,
  4210. addMoney: formData.checkBoxMoney,
  4211. addFeats: formData.checkBoxFeats,
  4212. addSpells: formData.checkBoxSpells,
  4213. adjustBlendedSlots: formData.checkBoxBlendedSlots,
  4214. addEquipment: formData.checkBoxEquipment,
  4215. addTreasure: formData.checkBoxTreasure,
  4216. addLores: formData.checkBoxLores,
  4217. addWeapons: formData.checkBoxWeapons,
  4218. addArmor: formData.checkBoxArmor,
  4219. addDeity: formData.checkBoxDeity,
  4220. addName: formData.checkBoxName,
  4221. addClass: formData.checkBoxClass,
  4222. addBackground: formData.checkBoxBackground,
  4223. addHeritage: formData.checkBoxHeritage,
  4224. addAncestry: formData.checkBoxAncestry,
  4225. addFamiliars: formData.checkBoxFamiliars,
  4226. addFormulas: formData.checkBoxFormulas,
  4227. statusCallback: PathmuncherImporter._updateProgress.bind(this),
  4228. };
  4229. src_logger.debug("Pathmuncher options", options);
  4230. await src_utils.setFlags(this.actor, options);
  4231. const statusBar = document.getElementById("pathmuncher-import-progress");
  4232. statusBar.classList.toggle("import-hidden");
  4233. const pathmuncher = new Pathmuncher(this.actor, options);
  4234. if (this.mode === "number") {
  4235. await pathmuncher.fetchPathbuilder(pathbuilderId);
  4236. } else if (this.mode === "json") {
  4237. try {
  4238. const jsonData = JSON.parse(formData.textBoxBuildJSON.trim());
  4239. pathmuncher.source = jsonData.build;
  4240. } catch (err) {
  4241. ui.notifications.error("Unable to parse JSON data");
  4242. return;
  4243. }
  4244. }
  4245. src_logger.debug("Pathmuncher Source", pathmuncher.source);
  4246. await pathmuncher.processCharacter();
  4247. src_logger.debug("Post processed character", pathmuncher);
  4248. await pathmuncher.updateActor();
  4249. src_logger.debug("Final import details", {
  4250. actor: this.actor,
  4251. pathmuncher,
  4252. options,
  4253. pathbuilderSource: pathmuncher.source,
  4254. pathbuilderId,
  4255. });
  4256. if (options.addFamiliars) {
  4257. const petShop = new PetShop({
  4258. type: "familiar",
  4259. parent: this.actor,
  4260. pathbuilderJson: pathmuncher.source
  4261. });
  4262. await petShop.processPets();
  4263. await petShop.addPetEffects();
  4264. }
  4265. this.close();
  4266. await pathmuncher.postImportCheck();
  4267. }
  4268. }
  4269. ;// CONCATENATED MODULE: ./src/hooks/api.js
  4270. function registerAPI() {
  4271. game.modules.get(constants.MODULE_NAME).api = {
  4272. Pathmuncher: Pathmuncher,
  4273. PathmuncherImporter: PathmuncherImporter,
  4274. PetShop: PetShop,
  4275. CompendiumMatcher: CompendiumMatcher,
  4276. Seasoning: Seasoning,
  4277. CompendiumSelector: CompendiumSelector,
  4278. data: {
  4279. generateFeatMap: FEAT_RENAME_MAP,
  4280. equipment: EQUIPMENT_RENAME_MAP,
  4281. restrictedEquipment: RESTRICTED_EQUIPMENT,
  4282. feats: FEAT_RENAME_MAP(),
  4283. },
  4284. utils: src_utils,
  4285. CONSTANTS: constants,
  4286. };
  4287. }
  4288. ;// CONCATENATED MODULE: ./src/hooks/settings.js
  4289. async function resetSettings() {
  4290. for (const [name, data] of Object.entries(constants.GET_DEFAULT_SETTINGS())) {
  4291. // eslint-disable-next-line no-await-in-loop
  4292. await game.settings.set(constants.MODULE_NAME, name, data.default);
  4293. }
  4294. window.location.reload();
  4295. }
  4296. class ResetSettingsDialog extends FormApplication {
  4297. constructor(...args) {
  4298. super(...args);
  4299. // eslint-disable-next-line no-constructor-return
  4300. return new Dialog({
  4301. title: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Title`),
  4302. content: `<p class="${constants.FLAG_NAME}-dialog-important">${game.i18n.localize(
  4303. `${constants.FLAG_NAME}.Dialogs.ResetSettings.Content`
  4304. )}</p>`,
  4305. buttons: {
  4306. confirm: {
  4307. icon: '<i class="fas fa-check"></i>',
  4308. label: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Confirm`),
  4309. callback: () => {
  4310. resetSettings();
  4311. },
  4312. },
  4313. cancel: {
  4314. icon: '<i class="fas fa-times"></i>',
  4315. label: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Cancel`),
  4316. },
  4317. },
  4318. default: "cancel",
  4319. });
  4320. }
  4321. }
  4322. function registerSettings() {
  4323. game.settings.registerMenu(constants.MODULE_NAME, "resetToDefaults", {
  4324. name: `${constants.FLAG_NAME}.Settings.Reset.Title`,
  4325. label: `${constants.FLAG_NAME}.Settings.Reset.Label`,
  4326. hint: `${constants.FLAG_NAME}.Settings.Reset.Hint`,
  4327. icon: "fas fa-refresh",
  4328. type: ResetSettingsDialog,
  4329. restricted: true,
  4330. });
  4331. for (const [name, data] of Object.entries(constants.GET_DEFAULT_SETTINGS())) {
  4332. game.settings.register(constants.MODULE_NAME, name, data);
  4333. }
  4334. game.settings.registerMenu(constants.MODULE_NAME, "selectCustomCompendiums", {
  4335. name: `${constants.FLAG_NAME}.Settings.UseCustomCompendiumMappings.Title`,
  4336. label: `${constants.FLAG_NAME}.Settings.UseCustomCompendiumMappings.Label`,
  4337. hint: `${constants.FLAG_NAME}.Settings.UseCustomCompendiumMappings.Hint`,
  4338. icon: "fas fa-book",
  4339. type: CompendiumSelector,
  4340. restricted: true,
  4341. });
  4342. }
  4343. ;// CONCATENATED MODULE: ./src/hooks/sheets.js
  4344. function registerSheetButton() {
  4345. const trustedUsersOnly = src_utils.setting("RESTRICT_TO_TRUSTED");
  4346. if (trustedUsersOnly && !game.user.isTrusted) return;
  4347. /**
  4348. * Character sheets
  4349. */
  4350. const pcSheetNames = Object.values(CONFIG.Actor.sheetClasses.character)
  4351. .map((sheetClass) => sheetClass.cls)
  4352. .map((sheet) => sheet.name);
  4353. pcSheetNames.forEach((sheetName) => {
  4354. Hooks.on("render" + sheetName, (app, html, data) => {
  4355. // only for GMs or the owner of this character
  4356. if (!data.owner || !data.actor) return;
  4357. const button = $(`<a class="pathmuncher-open" title="${constants.MODULE_FULL_NAME}"><i class="fas fa-hat-wizard"></i> ${constants.MODULE_FULL_NAME}</a>`);
  4358. button.click(() => {
  4359. if (game.user.can("ACTOR_CREATE")) {
  4360. const muncher = new PathmuncherImporter(PathmuncherImporter.defaultOptions, data.actor);
  4361. muncher.render(true);
  4362. } else {
  4363. ui.notifications.warn(game.i18n.localize(`${constants.FLAG_NAME}.Notifications.CreateActorPermission`), { permanent: true });
  4364. }
  4365. });
  4366. html.closest('.app').find('.pathmuncher-open').remove();
  4367. let titleElement = html.closest('.app').find('.window-title');
  4368. if (!app._minimized) button.insertAfter(titleElement);
  4369. });
  4370. });
  4371. }
  4372. ;// CONCATENATED MODULE: ./src/module.js
  4373. Hooks.once("init", () => {
  4374. registerSettings();
  4375. });
  4376. Hooks.once("ready", () => {
  4377. registerSheetButton();
  4378. registerAPI();
  4379. });
  4380. /******/ })()
  4381. ;
  4382. //# sourceMappingURL=main.js.map