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.

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