/******/ (() => { // webpackBootstrap /******/ "use strict"; var __webpack_exports__ = {}; ;// CONCATENATED MODULE: ./src/constants.js const debouncedReload = foundry.utils.debounce(() => window.location.reload(), 100); const CONSTANTS = { MODULE_NAME: "pathmuncher", MODULE_FULL_NAME: "Pathmuncher", FLAG_NAME: "pathmuncher", SETTINGS: { // Enable options LOG_LEVEL: "log-level", RESTRICT_TO_TRUSTED: "restrict-to-trusted", ADD_VISION_FEATS: "add-vision-feats", USE_CUSTOM_COMPENDIUM_MAPPINGS: "use-custom-compendium-mappings", CUSTOM_COMPENDIUM_MAPPINGS: "custom-compendium-mappings", USE_IMMEDIATE_DEEP_DIVE: "use-immediate-deep-dive", }, FEAT_PRIORITY: [ "Heritage", "Heritage Feat", "Ancestry", "Ancestry Feat", "Background", "Background Feat", "Class Feat", "Skill Feat", "General Feat", "Awarded Feat", ], ACTOR_FLAGS: { pathbuilderId: undefined, addFeats: true, addEquipment: true, addBackground: true, addHeritage: true, addAncestry: true, addSpells: true, adjustBlendedSlots: true, addMoney: true, addTreasure: true, addLores: true, addWeapons: true, addArmor: true, addDeity: true, addName: true, addClass: true, addFamiliars: true, addFormulas: true, }, CORE_COMPENDIUM_MAPPINGS: { feats: [ "pf2e.feats-srd", "pf2e-playtest-data.war-of-immortals-playtest-class-feats", "pf2e-legacy-content.feats-legacy", ], ancestryFeatures: ["pf2e.ancestryfeatures", "pf2e-legacy-content.ancestry-features-legacy"], classFeatures: [ "pf2e.classfeatures", "pf2e-playtest-data.war-of-immortals-playtest-class-features", "pf2e-legacy-content.class-features-legacy", ], actions: [ "pf2e.actionspf2e", "pf2e-playtest-data.war-of-immortals-playtest-actions", "pf2e-legacy-content.actions-legacy", ], spells: [ "pf2e-psychic-amps.psychic-psi-cantrips", "pf2e.spells-srd", "pf2e-playtest-data.war-of-immortals-playtest-spells", "pf2e-legacy-content.spells-legacy", ], classes: [ "pf2e.classes", "pf2e-playtest-data.war-of-immortals-playtest-classes", "pf2e-legacy-content.classes-legacy", ], ancestries: ["pf2e.ancestries", "pf2e-legacy-content.ancestries-legacy"], heritages: ["pf2e.heritages", "pf2e-legacy-content.heritages-legacy"], equipment: ["pf2e.equipment-srd", "pf2e-legacy-content.equipment-legacy"], formulas: ["pf2e.equipment-srd", "pf2e-legacy-content.formulas-legacy"], deities: ["pf2e.deities", "pf2e-legacy-content.deities-legacy"], backgrounds: ["pf2e.backgrounds", "pf2e-legacy-content.backgrounds-legacy"], }, GET_DEFAULT_SETTINGS() { return foundry.utils.deepClone(CONSTANTS.DEFAULT_SETTINGS); }, }; CONSTANTS.DEFAULT_SETTINGS = { // Enable options [CONSTANTS.SETTINGS.RESTRICT_TO_TRUSTED]: { name: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Name`, hint: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Hint`, scope: "world", config: true, type: Boolean, default: false, onChange: debouncedReload, }, [CONSTANTS.SETTINGS.USE_CUSTOM_COMPENDIUM_MAPPINGS]: { name: `${CONSTANTS.FLAG_NAME}.Settings.UseCustomCompendiumMappings.Name`, scope: "world", config: false, type: Boolean, default: false, }, [CONSTANTS.SETTINGS.USE_IMMEDIATE_DEEP_DIVE]: { name: `${CONSTANTS.FLAG_NAME}.Settings.UseImmediateDeepDive.Name`, scope: "world", config: false, type: Boolean, default: true, }, [CONSTANTS.SETTINGS.CUSTOM_COMPENDIUM_MAPPINGS]: { scope: "world", config: false, type: Object, default: { feats: [ "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-feats", "battlezoo-ancestries-year-of-monsters-pf2e.yom-features", "battlezoo-ancestries-year-of-monsters-pf2e.yom-feats", "clerics.clerics-feats", "clerics.clerics-features", "pf2e.feats-srd", "pf2e-playtest-data.war-of-immortals-playtest-class-feats", ], ancestryFeatures: [ "battlezoo-ancestries-year-of-monsters-pf2e.yom-features", "pf2e.ancestryfeatures", ], classFeatures: [ "battlezoo-ancestries-year-of-monsters-pf2e.yom-features", "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-feats", "battlezoo-ancestries-year-of-monsters-pf2e.yom-feats", "clerics.clerics-doctrines", "clerics.clerics-feats", "clerics.clerics-features", "pf2e.classfeatures", "pf2e-playtest-data.war-of-immortals-playtest-class-features", ], actions: ["pf2e.actionspf2e", "pf2e-playtest-data.war-of-immortals-playtest-actions"], spells: ["pf2e-psychic-amps.psychic-psi-cantrips", "pf2e.spells-srd", "pf2e-playtest-data.war-of-immortals-playtest-spells"], classes: ["clerics.clerics-features", "pf2e.classes", "pf2e-playtest-data.war-of-immortals-playtest-classes"], ancestries: [ "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-ancestry", "battlezoo-ancestries-year-of-monsters-pf2e.yom-ancestries", "pf2e.ancestries", ], heritages: [ "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-heritages", "battlezoo-ancestries-year-of-monsters-pf2e.yom-heritages", "pf2e.heritages", ], equipment: [ "battlezoo-ancestries-dragons-pf2e.pf2e-battlezoo-dragon-equipment", "battlezoo-ancestries-year-of-monsters-pf2e.yom-equipment", "pf2e.equipment-srd" ], formulas: ["pf2e.equipment-srd"], deities: ["clerics.clerics-deities", "pf2e.deities"], backgrounds: ["pf2e.backgrounds"], }, }, [CONSTANTS.SETTINGS.ADD_VISION_FEATS]: { name: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Name`, hint: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Hint`, scope: "player", config: true, type: Boolean, default: false, }, // debug [CONSTANTS.SETTINGS.LOG_LEVEL]: { name: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Name`, hint: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Hint`, scope: "world", config: true, type: String, choices: { DEBUG: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.debug`, INFO: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.info`, WARN: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.warn`, ERR: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.error`, OFF: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.off`, }, default: "WARN", } }; CONSTANTS.PATH = `modules/${CONSTANTS.MODULE_NAME}`; /* harmony default export */ const constants = (CONSTANTS); ;// CONCATENATED MODULE: ./src/utils.js const utils = { isObject: (obj) => { return typeof obj === 'object' && !Array.isArray(obj) && obj !== null; }, isString: (str) => { return typeof str === 'string' || str instanceof String; }, wait: async (ms) => { return new Promise((resolve) => { setTimeout(resolve, ms); }); }, capitalize: (s) => { if (typeof s !== "string") return ""; return s.charAt(0).toUpperCase() + s.slice(1); }, setting: (key) => { return game.settings.get(constants.MODULE_NAME, constants.SETTINGS[key]); }, updateSetting: async (key, value) => { return game.settings.set(constants.MODULE_NAME, constants.SETTINGS[key], value); }, getFlags: (actor) => { const flags = actor.flags[constants.FLAG_NAME] ? actor.flags[constants.FLAG_NAME] : constants.ACTOR_FLAGS; return flags; }, setFlags: async (actor, flags) => { let updateData = {}; setProperty(updateData, `flags.${constants.FLAG_NAME}`, flags); await actor.update(updateData); return actor; }, resetFlags: async (actor) => { return utils.setFlags(actor, null); }, getOrCreateFolder: async (root, entityType, folderName, folderColor = "") => { let folder = game.folders.contents.find((f) => f.type === entityType && f.name === folderName // if a root folder we want to match the root id for the parent folder && (root ? root.id : null) === (f.folder?.id ?? null) ); // console.warn(`Looking for ${root} ${entityType} ${folderName}`); // console.warn(folder); if (folder) return folder; folder = await Folder.create( { name: folderName, type: entityType, color: folderColor, parent: (root) ? root.id : null, }, { displaySheet: false } ); return folder; }, // eslint-disable-next-line no-unused-vars getFolder: async (kind, subFolder = "", baseFolderName = "Pathmuncher", baseColor = "#6f0006", subColor = "#98020a", typeFolder = true) => { let entityTypes = new Map(); entityTypes.set("pets", "Pets"); const folderName = game.i18n.localize(`${constants.MODULE_NAME}.labels.${kind}`); const entityType = entityTypes.get(kind); const baseFolder = await utils.getOrCreateFolder(null, entityType, baseFolderName, baseColor); const entityFolder = typeFolder ? await utils.getOrCreateFolder(baseFolder, entityType, folderName, subColor) : baseFolder; if (subFolder !== "") { const subFolderName = subFolder.charAt(0).toUpperCase() + subFolder.slice(1); const typeFolder = await utils.getOrCreateFolder(entityFolder, entityType, subFolderName, subColor); return typeFolder; } else { return entityFolder; } }, allowDualClasses: () => { return (isNewerVersion("5.9.0", game.version) && game.settings.get("pf2e", "dualClassVariant")); // || (!isNewerVersion("5.9.0", game.version) && when remaster supports dualclass then add here }, allowAncestryParagon: () => { return (isNewerVersion("5.9.0", game.version) && game.settings.get("pf2e", "ancestryParagonVariant")); } }; /* harmony default export */ const src_utils = (utils); ;// CONCATENATED MODULE: ./src/logger.js const logger = { _showMessage: (logLevel, data) => { if (!logLevel || !data || typeof logLevel !== "string") { return false; } const setting = src_utils.setting("LOG_LEVEL"); const logLevels = ["DEBUG", "INFO", "WARN", "ERR", "OFF"]; const logLevelIndex = logLevels.indexOf(logLevel.toUpperCase()); if (setting == "OFF" || logLevelIndex === -1 || logLevelIndex < logLevels.indexOf(setting)) { return false; } return true; }, log: (logLevel, ...data) => { if (!logger._showMessage(logLevel, data)) { return; } logLevel = logLevel.toUpperCase(); let msg = "No logging message provided. Please see the payload for more information."; let payload = data.slice(); if (data[0] && typeof (data[0] == "string")) { msg = data[0]; if (data.length > 1) { payload = data.slice(1); } else { payload = null; } } msg = `${constants.MODULE_NAME} | ${logLevel} > ${msg}`; switch (logLevel) { case "DEBUG": if (payload) { console.debug(msg, ...payload); // eslint-disable-line no-console } else { console.debug(msg); // eslint-disable-line no-console } break; case "INFO": if (payload) { console.info(msg, ...payload); // eslint-disable-line no-console } else { console.info(msg); // eslint-disable-line no-console } break; case "WARN": if (payload) { console.warn(msg, ...payload); // eslint-disable-line no-console } else { console.warn(msg); // eslint-disable-line no-console } break; case "ERR": if (payload) { console.error(msg, ...payload); // eslint-disable-line no-console } else { console.error(msg); // eslint-disable-line no-console } break; default: break; } }, debug: (...data) => { logger.log("DEBUG", ...data); }, info: (...data) => { logger.log("INFO", ...data); }, warn: (...data) => { logger.log("WARN", ...data); }, error: (...data) => { logger.log("ERR", ...data); }, }; /* harmony default export */ const src_logger = (logger); ;// CONCATENATED MODULE: ./src/data/equipment.js const SWAPS = [ /^(Greater) (.*)/, /^(Lesser) (.*)/, /^(Major) (.*)/, /^(Moderate) (.*)/, /^(Standard) (.*)/, ]; const POSTFIX_PB_REMOVALS = [ /(.*) (- Melee)$/, /(.*) (- Ranged)$/, /(Charm of Resistance .*) - (.*)/, ]; const PARENTHESIS = [ /^(.*) \((.*)\)$/, ]; const SPLITS = [ // /^(.*) - (.*)/, ]; const SPLITS_INVERT = [ /^(.*): (.*)/, ]; const REPLACES = [ { pbName: "Ring of Energy Resistance", foundryName: "Charm of Resistance" }, { pbName: "Feather Token", foundryName: "Marvelous Miniatures" }, { pbName: "Goggles of Night", foundryName: "Obsidian Goggles" }, ]; // this equipment is named differently in foundry vs pathbuilder const EQUIPMENT_RENAME_STATIC_MAP = [ { pbName: "Inventor Power Suit", foundryName: "Power Suit" }, { pbName: "Inventor Power Suit (Heavy)", foundryName: "Power Suit" }, { pbName: "Inventor Subterfuge Suit", foundryName: "Subterfuge Suit" }, { pbName: "Chain", foundryName: "Chain (10 feet)" }, { pbName: "Oil", foundryName: "Oil (1 pint)" }, { pbName: "Bracelets of Dashing", foundryName: "Bracelet of Dashing" }, { pbName: "Fingerprinting Kit", foundryName: "Fingerprint Kit" }, { pbName: "Ladder", foundryName: "Ladder (10-foot)" }, { pbName: "Mezmerizing Opal", foundryName: "Mesmerizing Opal" }, // { pbName: "Explorer's Clothing", foundryName: "Clothing (Explorer's)" }, { pbName: "Flaming Star (Greater)", foundryName: "Greater Flaming Star" }, { pbName: "Potion of Lesser Darkvision", foundryName: "Darkvision Elixir (Lesser)" }, { pbName: "Potion of Greater Darkvision", foundryName: "Darkvision Elixir (Greater)" }, { pbName: "Potion of Moderate Darkvision", foundryName: "Darkvision Elixir (Moderate)" }, { pbName: "Bottled Sunlight", foundryName: "Formulated Sunlight" }, { pbName: "Magazine (Repeating Hand Crossbow)", foundryName: "Magazine with 5 Bolts" }, { pbName: "Astrolabe (Standard)", foundryName: "Standard Astrolabe" }, { pbName: "Skinitch Salve", foundryName: "Skinstitch Salve" }, { pbName: "Flawless Scale", foundryName: "Abadar's Flawless Scale" }, { pbName: "Construct Key", foundryName: "Cordelia's Construct Key" }, { pbName: "Construct Key (Greater)", foundryName: "Cordelia's Greater Construct Key" }, { pbName: "Lesser Swapping Stone", foundryName: "Lesser Bonmuan Swapping Stone" }, { pbName: "Major Swapping Stone", foundryName: "Major Bonmuan Swapping Stone" }, { pbName: "Moderate Swapping Stone", foundryName: "Moderate Bonmuan Swapping Stone" }, { pbName: "Greater Swapping Stone", foundryName: "Greater Bonmuan Swapping Stone" }, { pbName: "Heartstone", foundryName: "Skarja's Heartstone" }, { pbName: "Bullets (10 rounds)", foundryName: "Sling Bullets" }, { pbName: "Hide", foundryName: "Hide Armor" }, { pbName: "Soverign Glue", foundryName: "Sovereign Glue" }, { pbName: "Axe Musket - Melee", foundryName: "Axe Musket" }, { pbName: "Axe Musket - Ranged", foundryName: "Axe Musket" }, { pbName: "Extendible Pincer", foundryName: "Extendable Pincer" }, { pbName: "Clothing (Explorer's)", foundryName: "Explorer's Clothing" }, { pbName: "Street Preacher [Placeholder]", foundryName: "Street Preacher" }, { pbName: "Repair Kit", foundryName: "Repair Toolkit" }, { pbName: "Repair Kit (Superb)", foundryName: "Repair Toolkit (Superb)" }, { pbName: "Alchemist's Tools", foundryName: "Alchemist's Toolkit" }, { pbName: "Healer's Tools", foundryName: "Healer's Toolkit" }, { pbName: "Healer's Tools (Expanded)", foundryName: "Healer's Toolkit (Expanded)" }, { pbName: "Thieves' Tools", foundryName: "Thieves' Toolkit" }, { pbName: "Thieves' Tools (Infiltrator)", foundryName: "Thieves' Toolkit (Infiltrator)" }, { pbName: "Thieves' Tools (Infiltrator Picks)", foundryName: "Thieves' Toolkit (Infiltrator Picks)" }, { pbName: "Artisan's Tools", foundryName: "Artisan's Toolkit" }, { pbName: "Artisan's Tools (Sterling)", foundryName: "Artisan's Toolkit (Sterling)" }, { pbName: "Aeon Stone (Dull Grey)", foundryName: "Aeon Stone (Consumed)" }, { pbName: "Aeon Stone (Clear Spindle)", foundryName: "Aeon Stone (Nourishing)" }, { pbName: "Aeon Stone (Tourmaline Sphere)", foundryName: "Aeon Stone (Delaying)" }, { pbName: "Aeon Stone (Orange Prism)", foundryName: "Aeon Stone (Amplifying)" }, { pbName: "Bag of Holding", foundryName: "Spacious Pouch" }, { pbName: "Barkskin Potion", foundryName: "Oak Potion" }, { pbName: "Boots of Speed", foundryName: "Propulsive Boots" }, { pbName: "Bracers of Armor", foundryName: "Bands of Force" }, { pbName: "Broom of Flying", foundryName: "Flying Broomstick" }, { pbName: "Dagger of Venom", foundryName: "Serpent Dagger" }, // these are actually matched to energy type witch Pathbuilder does not support { pbName: "Dragon's Breath Potion (Young)", foundryName: "Energy Breath Potion (Lesser)" }, { pbName: "Dragon's Breath Potion (Adult)", foundryName: "Energy Breath Potion (Moderate)" }, { pbName: "Dragon's Breath Potion (Wyrm)", foundryName: "Energy Breath Potion (Greater)" }, { pbName: "Druid's Vestments", foundryName: "Living Mantle" }, { pbName: "Everburning Torch", foundryName: "Everlight Crystal" }, { pbName: "Eyes of the Eagle", foundryName: "Eyes of the Cat" }, { pbName: "Feather Token (Chest)", foundryName: "Marvelous Miniatures (Chest)" }, { pbName: "Feather Token (Ladder)", foundryName: "Marvelous Miniatures (Ladder)" }, { pbName: "Feather Token (Swan Boat)", foundryName: "Marvelous Miniatures (Boat)" }, { pbName: "Flame Tongue", foundryName: "Searing Blade" }, { pbName: "Gloves of Storing", foundryName: "Retrieval Belt" }, { pbName: "Goggles of Night", foundryName: "Obsidian Goggles" }, { pbName: "Goggles of Night (Greater)", foundryName: "Obsidian Goggles (Greater)" }, { pbName: "Goggles of Night (Major)", foundryName: "Obsidian Goggles (Major)" }, { pbName: "Hat of Disguise", foundryName: "Masquerade Scarf" }, { pbName: "Hat of Disguise (Greater)", foundryName: "Masquerade Scarf (Greater)" }, { pbName: "Horn of Fog", foundryName: "Cloud Pouch" }, { pbName: "Horseshoes of Speed", foundryName: "Alacritous Horseshoes" }, { pbName: "Javelin of Lightning", foundryName: "Trident of Lightning" }, { pbName: "Potion of Expeditious Retreat", foundryName: "Potion of Emergency Escape" }, { pbName: "Ring of Energy Resistance (Greater)", foundryName: "Charm of Resistance (Greater)" }, { pbName: "Ring of Energy Resistance (Major)", foundryName: "Charm of Resistance (Major)" }, { pbName: "Silversheen", foundryName: "Silver Salve" }, { pbName: "Smokestick (Lesser)", foundryName: "Smoke Ball (Lesser)" }, { pbName: "Smokestick (Greater)", foundryName: "Smoke Ball (Greater)" }, { pbName: "Sunrod", foundryName: "Glow Rod" }, { pbName: "Tanglefoot Bag (Lesser)", foundryName: "Glue Bomb (Lesser)" }, { pbName: "Tanglefoot Bag (Moderate)", foundryName: "Glue Bomb (Moderate)" }, { pbName: "Tanglefoot Bag (Major)", foundryName: "Glue Bomb (Major)" }, { pbName: "Tanglefoot Bag (Greater)", foundryName: "Glue Bomb (Greater)" }, { pbName: "Tindertwig", foundryName: "Matchstick" }, { pbName: "Owlbear Claw", foundryName: "Predator's Claw" }, { pbName: "Wand of Manifold Missiles", foundryName: "Wand of Shardstorm" }, { pbName: "Wand of Manifold Missiles (1st-Level Spell)", foundryName: "Wand of Shardstorm (1st-Rank Spell)" }, { pbName: "Wand of Manifold Missiles (3rd-Level Spell)", foundryName: "Wand of Shardstorm (3rd-Rank Spell)" }, { pbName: "Wand of Manifold Missiles (5th-Level Spell)", foundryName: "Wand of Shardstorm (5th-Rank Spell)" }, { pbName: "Wand of Manifold Missiles (7th-Level Spell)", foundryName: "Wand of Shardstorm (7th-Rank Spell)" }, ]; function dynamicNamesSteps(pbName) { const result = []; for (const reg of POSTFIX_PB_REMOVALS) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: match[1], details: match[2] }); } } for (const reg of SWAPS) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] }); } } for (const reg of SPLITS) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: match[2], details: match[1] }); } } for (const reg of SPLITS_INVERT) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: match[1], details: match[2] }); } } for (const reg of PARENTHESIS) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: match[1], details: match[2] }); } } return result; } function generateDynamicNames(pbName) { const result = []; // if we have a hardcoded map, don't return here const basicResults = EQUIPMENT_RENAME_STATIC_MAP.filter((e) => e.pbName === pbName); if (basicResults.length > 0) { result.push(...basicResults); } for (const replace of REPLACES) { if (pbName.includes(replace.pbName)) { const replaced = pbName.replace(replace.pbName, replace.foundryName); result.push(...dynamicNamesSteps(replaced)); result.push({ pbName, foundryName: replaced }); } } if (result.length > 0) { return result; } result.push(...dynamicNamesSteps(pbName)); return result; } function EQUIPMENT_RENAME_MAP(pbName = null) { const postfixNames = pbName ? generateDynamicNames(pbName) : []; return postfixNames.concat(EQUIPMENT_RENAME_STATIC_MAP); } // this is equipment is special and shouldn't have the transformations applied to it const RESTRICTED_EQUIPMENT = [ "Bracers of Armor", ]; const IGNORED_EQUIPMENT = [ "Unarmored", "Inventor Power Suit", "Inventor Power Suit (Heavy)", "Inventor Subterfuge Suit", ]; const IGNORED_DISPLAY_POSTFIX = [ /(.*) - Melee$/, /(.*) - Ranged$/, ]; function IGNORED_EQUIPMENT_DISPLAY(pbName) { for (const reg of IGNORED_DISPLAY_POSTFIX) { const match = reg.test(pbName); if (match === true) return true; } return false; } const GRANTED_ITEMS_LIST = [ "Inventor Power Suit", "Inventor Power Suit (Heavy)", "Inventor Subterfuge Suit", ]; ;// CONCATENATED MODULE: ./src/data/features.js // these are features which are named differently in pathbuilder to foundry const SKILL_LOOKUP = { "acrobatics": "acr", "arcana": "arc", "athletics": "ath", "crafting": "cra", "deception": "dec", "diplomacy": "dip", "intimidation": "itm", "medicine": "med", "nature": "nat", "occultism": "occ", "performance": "prf", "religion": "rel", "society": "soc", "stealth": "ste", "survival": "sur", "thievery": "thi", }; const features_POSTFIX_PB_REMOVALS = [ /(.*) (Racket)$/, /(.*) (Style)$/, /(.*) (Initiate Benefit)$/, /(.*) Mystery$/, // Cleric + /(.*) (Doctrine)$/, /(.*) (Element)$/, /(.*) (Impulse Junction)$/, /(.*) (Gate Junction:).*$/, /(.*) (Patron)$/, ]; const PREFIX_PB_REMOVALS = [ /^(Arcane Thesis): (.*)/, /^(Arcane School): (.*)/, /^(The) (.*)/, // Cleric + /^(Blessing): (.*)/, /^(Empiricism) Selected Skill: (.*)/, ]; const POSTFIX_PB_SPLIT_AND_KEEP = [ /(.*) (Impulse Junction)$/, /(.*) Gate Junction: (.*)$/, ]; const features_PARENTHESIS = [ /^(.*) \((.*)\)$/, ]; const features_SPLITS = [ /^(.*): (.*)/, ]; const features_SWAPS = [ /^(Greater) (.*)/, /^(Lesser) (.*)/, /^(Major) (.*)/, /^(Moderate) (.*)/, /^(Standard) (.*)/, ]; const FEAT_RENAME_STATIC_MAP = [ { pbName: "Academic", foundryName: "Ustalavic Academic" }, { pbName: "Academic (Arcana)", foundryName: "Magaambya Academic" }, { pbName: "Academic (Nature)", foundryName: "Magaambya Academic" }, { pbName: "Aerialist", foundryName: "Shory Aerialist" }, { pbName: "Aeromancer", foundryName: "Shory Aeromancer" }, { pbName: "Ancient-Blooded", foundryName: "Ancient-Blooded Dwarf" }, { pbName: "Antipaladin [Chaotic Evil]", foundryName: "Antipaladin" }, { pbName: "Ape", foundryName: "Ape Animal Instinct" }, { pbName: "Aquatic Eyes (Darkvision)", foundryName: "Aquatic Eyes" }, { pbName: "Astrology", foundryName: "Saoc Astrology" }, { pbName: "Battle Ready", foundryName: "Battle-Ready Orc" }, { pbName: "Bite (Gnoll)", foundryName: "Bite" }, { pbName: "Bloodline: Genie (Efreeti)", foundryName: "Bloodline: Genie" }, { pbName: "Bloody Debilitations", foundryName: "Bloody Debilitation" }, { pbName: "Canoneer", foundryName: "Cannoneer" }, { pbName: "Cave Climber Kobold", foundryName: "Caveclimber Kobold" }, { pbName: "Child of Squalor", foundryName: "Child of the Puddles" }, { pbName: "Chosen One", foundryName: "Chosen of Lamashtu" }, { pbName: "Cognative Mutagen (Greater)", foundryName: "Cognitive Mutagen (Greater)" }, { pbName: "Cognative Mutagen (Lesser)", foundryName: "Cognitive Mutagen (Lesser)" }, { pbName: "Cognative Mutagen (Major)", foundryName: "Cognitive Mutagen (Major)" }, { pbName: "Cognative Mutagen (Moderate)", foundryName: "Cognitive Mutagen (Moderate)" }, { pbName: "Cognitive Crossover", foundryName: "Kreighton's Cognitive Crossover" }, { pbName: "Collegiate Attendant Dedication", foundryName: "Magaambyan Attendant Dedication" }, { pbName: "Construct Carver", foundryName: "Tupilaq Carver" }, { pbName: "Cunning Stance", foundryName: "Devrin's Cunning Stance" }, { pbName: "Constructed (Android)", foundryName: "Constructed" }, { pbName: "Dazzling Diversion", foundryName: "Devrin's Dazzling Diversion" }, { pbName: "Deadly Hair", foundryName: "Syu Tak-nwa's Deadly Hair" }, { pbName: "Deepvision", foundryName: "Deep Vision" }, { pbName: "Deflect Arrows", foundryName: "Deflect Arrow" }, { pbName: "Desecrator [Neutral Evil]", foundryName: "Desecrator" }, { pbName: "Detective Dedication", foundryName: "Edgewatch Detective Dedication" }, { pbName: "Duelist Dedication (LO)", foundryName: "Aldori Duelist Dedication" }, { pbName: "Dwarven Hold Education", foundryName: "Dongun Education" }, { pbName: "Ember's Eyes (Darkvision)", foundryName: "Ember's Eyes" }, { pbName: "Enhanced Familiar Feat", foundryName: "Enhanced Familiar" }, { pbName: "Enhanced Fire", foundryName: "Artokus's Fire" }, { pbName: "Enigma", foundryName: "Enigma Muse" }, { pbName: "Escape", foundryName: "Fane's Escape" }, { pbName: "Eye of the Arcane Lords", foundryName: "Eye of the Arclords" }, { pbName: "Flip", foundryName: "Farabellus Flip" }, { pbName: "Fourberie", foundryName: "Fane's Fourberie" }, { pbName: "Ganzi Gaze (Low-Light Vision)", foundryName: "Ganzi Gaze" }, { pbName: "Guild Agent Dedication", foundryName: "Pathfinder Agent Dedication" }, { pbName: "Harmful Font", foundryName: "Divine Font" }, { pbName: "Green Watcher", foundryName: "Greenwatcher" }, { pbName: "Green Watch Initiate", foundryName: "Greenwatch Initiate" }, { pbName: "Green Watch Veteran", foundryName: "Greenwatch Veteran" }, { pbName: "Healing Font", foundryName: "Divine Font" }, { pbName: "Heatwave", foundryName: "Heat Wave" }, { pbName: "Heavenseeker Dedication", foundryName: "Jalmeri Heavenseeker Dedication" }, { pbName: "Heir of the Astrologers", foundryName: "Heir of the Saoc" }, { pbName: "High Killer Training", foundryName: "Vernai Training" }, { pbName: "Ice-Witch", foundryName: "Irriseni Ice-Witch" }, { pbName: "Impeccable Crafter", foundryName: "Impeccable Crafting" }, { pbName: "Incredible Beastmaster's Companion", foundryName: "Incredible Beastmaster Companion" }, { pbName: "Interrogation", foundryName: "Bolera's Interrogation" }, { pbName: "Katana", foundryName: "Katana Weapon Familiarity" }, { pbName: "Liberator [Chaotic Good]", foundryName: "Liberator" }, { pbName: "Lumberjack Dedication", foundryName: "Turpin Rowe Lumberjack Dedication" }, { pbName: "Lumberjack", foundryName: "Lumber Consortium Laborer" }, { pbName: "Maestro", foundryName: "Maestro Muse" }, { pbName: "Major Lesson I", foundryName: "Major Lesson" }, { pbName: "Major Lesson II", foundryName: "Major Lesson" }, { pbName: "Major Lesson III", foundryName: "Major Lesson" }, { pbName: "Mantis God's Grip", foundryName: "Achaekek's Grip" }, { pbName: "Marked for Death", foundryName: "Mark for Death" }, { pbName: "Miraculous Spells", foundryName: "Miraculous Spell" }, { pbName: "Multifarious", foundryName: "Multifarious Muse" }, { pbName: "Mystic", foundryName: "Nexian Mystic" }, { pbName: "Paladin [Lawful Good]", foundryName: "Paladin" }, { pbName: "Parry", foundryName: "Aldori Parry" }, { pbName: "Polymath", foundryName: "Polymath Muse" }, { pbName: "Precise Debilitation", foundryName: "Precise Debilitations" }, { pbName: "Prodigy", foundryName: "Merabite Prodigy" }, { pbName: "Quick Climber", foundryName: "Quick Climb" }, { pbName: "Raider", foundryName: "Ulfen Raider" }, { pbName: "Recognise Threat", foundryName: "Recognize Threat" }, { pbName: "Redeemer [Neutral Good]", foundryName: "Redeemer" }, { pbName: "Revivification Protocall", foundryName: "Revivification Protocol" }, { pbName: "Riposte", foundryName: "Aldori Riposte" }, { pbName: "Rkoan Arts", foundryName: "Rokoan Arts" }, { pbName: "Saberteeth", foundryName: "Saber Teeth" }, { pbName: "Scholarly Recollection", foundryName: "Uzunjati Recollection" }, { pbName: "Scholarly Storytelling", foundryName: "Uzunjati Storytelling" }, { pbName: "Shamanic Adherent", foundryName: "Rivethun Adherent" }, { pbName: "Shamanic Disciple", foundryName: "Rivethun Disciple" }, { pbName: "Shamanic Spiritual Attunement", foundryName: "Rivethun Spiritual Attunement" }, { pbName: "Skysage Dedication", foundryName: "Oatia Skysage Dedication" }, { pbName: "Secret Lesson", foundryName: "Janatimo's Lessons" }, { pbName: "Sentry Dedication", foundryName: "Lastwall Sentry Dedication" }, { pbName: "Stab and Snag", foundryName: "Stella's Stab and Snag" }, { pbName: "Tenets of Evil", foundryName: "The Tenets of Evil" }, { pbName: "Tenets of Good", foundryName: "The Tenets of Good" }, { pbName: "Tongue of the Sun and Moon", foundryName: "Tongue of Sun and Moon" }, { pbName: "Tribal Bond", foundryName: "Quah Bond" }, { pbName: "Tyrant [Lawful Evil]", foundryName: "Tyrant" }, { pbName: "Vestigal Wings", foundryName: "Vestigial Wings" }, { pbName: "Virtue-Forged Tattooed", foundryName: "Virtue-Forged Tattoos" }, { pbName: "Wakizashi", foundryName: "Wakizashi Weapon Familiarity" }, { pbName: "Warden", foundryName: "Lastwall Warden" }, { pbName: "Warrior", foundryName: "Warrior Muse" }, { pbName: "Wary Eye", foundryName: "Eye of Ozem" }, { pbName: "Wayfinder Resonance Infiltrator", foundryName: "Westyr's Wayfinder Repository" }, { pbName: "Wind God's Fan", foundryName: "Wind God’s Fan" }, { pbName: "Wind God’s Fan", foundryName: "Wind God's Fan" }, // dragons { pbName: "Black", foundryName: "Black Dragon" }, { pbName: "Brine", foundryName: "Brine Dragon" }, { pbName: "Copper", foundryName: "Copper Dragon" }, { pbName: "Blue", foundryName: "Blue Dragon" }, { pbName: "Bronze", foundryName: "Bronze Dragon" }, { pbName: "Cloud", foundryName: "Cloud Dragon" }, { pbName: "Sky", foundryName: "Sky Dragon" }, { pbName: "Brass", foundryName: "Brass Dragon" }, { pbName: "Underworld", foundryName: "Underworld Dragon" }, { pbName: "Crystal", foundryName: "Crystal Dragon" }, { pbName: "Forest", foundryName: "Forest Dragon" }, { pbName: "Green", foundryName: "Green Dragon" }, { pbName: "Sea", foundryName: "Sea Dragon" }, { pbName: "Silver", foundryName: "Silver Dragon" }, { pbName: "White", foundryName: "White Dragon" }, { pbName: "Sovereign", foundryName: "Sovereign Dragon" }, { pbName: "Umbral", foundryName: "Umbral Dragon" }, { pbName: "Red", foundryName: "Red Dragon" }, { pbName: "Gold", foundryName: "Gold Dragon" }, { pbName: "Magma", foundryName: "Magma Dragon" }, // sizes for fleshwarp { pbName: "Medium", foundryName: "med" }, { pbName: "Small", foundryName: "sm" }, // Cleric + { pbName: "Decree of the Warsworn Ecstacy", foundryName: "Decree of Warsworn Ecstacy" }, { pbName: "Decree of Warsworn Ecstacy", foundryName: "Decree of the Warsworn Ecstacy" }, // remaster { pbName: "Lightning Reflexes", foundryName: "Reflex Expertise" }, { pbName: "Great Fortitude", foundryName: "Fortitude Expertise" }, { pbName: "Iron Will", foundryName: "Will Expertise" }, { pbName: "Alertness", foundryName: "Perception Expertise" }, { pbName: "Incredible Senses", foundryName: "Perception Legend" }, { pbName: "Vigilant Senses", foundryName: "Perception Mastery" }, ]; function features_generateDynamicNames(pbName) { const result = []; // if we have a hardcoded map, don't return here if (FEAT_RENAME_STATIC_MAP.some((e) => e.pbName === pbName)) return result; for (const reg of features_POSTFIX_PB_REMOVALS) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: match[1], details: match[2] }); } } for (const reg of PREFIX_PB_REMOVALS) { const match = pbName.match(reg); if (match) { const parsed = { pbName, foundryName: match[2], details: match[1] }; parsed.foundryValue = SKILL_LOOKUP[parsed.foundryName.toLowerCase()]; result.push(parsed); } } for (const reg of features_SPLITS) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: match[2], details: match[1] }); } } for (const reg of features_PARENTHESIS) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: match[1], details: match[2] }); } } for (const reg of features_SWAPS) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] }); } } return result; } function FEAT_RENAME_MAP(pbName = null) { const postfixNames = pbName ? features_generateDynamicNames(pbName) : []; return postfixNames.concat(FEAT_RENAME_STATIC_MAP); } const SHARED_IGNORE_LIST = [ "Draconic Rage", // just handled by effects on Draconic Instinct "Mirror Initiate Benefit", "Spellstrike Specifics", "Unarmored", "Simple Weapon Expertise", "Spellbook", "Titan Mauler", // not needed "Energy Emanation", // pathbuilder does not pass through a type for this "Imprecise Sense", // this gets picked up and added by granted features "Imprecise Scent", // this gets picked up and added by granted features "Sanctification", // choose on import ]; const IGNORED_FEATS_LIST = [ // ignore skills listed as feats "Acrobatics", "Athletics", "Deception", "Intimidation", "Nature", "Performance", "Society", "Survival", "Arcana", "Crafting", "Diplomacy", "Medicine", "Occultism", "Religion", "Stealth", "Thievery", // sizes // "Medium", // "Small", ]; const IGNORED_SPECIALS_LIST = [ ]; function IGNORED_FEATS() { // const visionFeats = utils.setting("ADD_VISION_FEATS") ? [] : ["Low-Light Vision", "Darkvision"]; return IGNORED_FEATS_LIST.concat(SHARED_IGNORE_LIST); } function IGNORED_SPECIALS() { const visionFeats = src_utils.setting("ADD_VISION_FEATS") ? [] : ["Low-Light Vision", "Darkvision"]; return IGNORED_SPECIALS_LIST.concat(SHARED_IGNORE_LIST, visionFeats); } function specialOnlyNameLookup(name) { for (const [key, value] of Object.entries(SKILL_LOOKUP)) { if (key === name.toLowerCase()) { return { pbName: name, foundryName: name, foundryValue: value }; } } return undefined; } function SPECIAL_NAME_ADDITIONS(specials) { const newSpecials = []; for (const special of specials) { for (const reg of POSTFIX_PB_SPLIT_AND_KEEP) { const match = special.match(reg); if (match) { newSpecials.push(match[2]); } } } return newSpecials; } const NO_AUTO_CHOICE_LIST = [ // "Elemental Evolution", ]; function NO_AUTO_CHOICE() { return NO_AUTO_CHOICE_LIST; } ;// CONCATENATED MODULE: ./src/data/spells.js const FEAT_SPELLCASTING = [ { name: "Kitsune Spell Familiarity", showSlotless: false, knownSpells: ["Daze", "Forbidding Ward", "Ghost Sound"], preparePBSpells: true, }, { name: "Kitsune Spell Expertise", showSlotless: false, knownSpells: ["Confusion", "Death Ward", "Illusory Scene"], preparePBSpells: true, }, { name: "Kitsune Spell Mysteries", showSlotless: false, knownSpells: ["Bane", "Illusory Object", "Sanctuary"], preparePBSpells: true, }, { name: "Nagaji Spell Familiarity", showSlotless: false, knownSpells: ["Daze", "Detect Magic", "Mage Hand"], preparePBSpells: true, }, { name: "Nagaji Spell Expertise", showSlotless: false, knownSpells: ["Blink", "Control Water", "Subconscious Suggestion"], preparePBSpells: true, }, { name: "Nagaji Spell Mysteries", showSlotless: false, knownSpells: ["Charm", "Fleet Step", "Heal"], preparePBSpells: true, }, { name: "Rat Magic", showSlotless: false, knownSpells: [], preparePBSpells: true, }, ]; const REMASTER_NAMES = [ { pbName: "Animate Dead", foundryName: "Summon Undead" }, { pbName: "Augment Summoning", foundryName: "Fortify Summoning" }, { pbName: "Baleful Polymorph", foundryName: "Cursed Metamorphosis" }, { pbName: "Barkskin", foundryName: "Oaken Resilience" }, { pbName: "Bind Soul", foundryName: "Seize Soul" }, { pbName: "Blind Ambition", foundryName: "Ignite Ambition" }, { pbName: "Blink", foundryName: "Flicker" }, { pbName: "Burning Hands", foundryName: "Breathe Fire" }, { pbName: "Calm Emotions", foundryName: "Calm" }, { pbName: "Charming Words", foundryName: "Charming Push" }, { pbName: "Chill Touch", foundryName: "Void Warp" }, { pbName: "Cloudkill", foundryName: "Toxic Cloud" }, { pbName: "Color Spray", foundryName: "Dizzying Colors" }, { pbName: "Commune with Nature", foundryName: "Commune" }, { pbName: "Comprehend Language", foundryName: "Translate" }, { pbName: "Continual Flame", foundryName: "Everlight" }, { pbName: "Crushing Despair", foundryName: "Wave of Despair" }, { pbName: "Dancing Lights", foundryName: "Light" }, { pbName: "Dimension Door", foundryName: "Translocate" }, { pbName: "Dimensional Anchor", foundryName: "Planar Seal" }, { pbName: "Dimensional Lock", foundryName: "Planar Tether" }, { pbName: "Discern Location", foundryName: "Pinpoint" }, { pbName: "Disrupt Undead", foundryName: "Vitality Lash" }, { pbName: "Disrupting Weapons", foundryName: "Infuse Vitality" }, { pbName: "Endure Elements", foundryName: "Environmental Endurance" }, { pbName: "Entangle", foundryName: "Entangling Flora" }, { pbName: "False Life", foundryName: "False Vitality" }, { pbName: "Feather Fall", foundryName: "Gentle Landing" }, { pbName: "Feeblemind", foundryName: "Never Mind" }, { pbName: "Finger of Death", foundryName: "Execute" }, { pbName: "Flaming Sphere", foundryName: "Floating Flame" }, { pbName: "Flesh To Stone", foundryName: "Petrify" }, { pbName: "Freedom of Movement", foundryName: "Unfettered Movement" }, { pbName: "Gaseous Form", foundryName: "Vapor Form" }, { pbName: "Gentle Repose", foundryName: "Peaceful Rest" }, { pbName: "Glibness", foundryName: "Honeyed Words" }, { pbName: "Glitterdust", foundryName: "Revealing Light" }, { pbName: "Globe of Invulnerability", foundryName: "Dispelling Globe" }, { pbName: "Hallucinatory Terrain", foundryName: "Mirage" }, { pbName: "Hideous Laughter", foundryName: "Laughing Fit" }, { pbName: "Horrid Wilting", foundryName: "Desiccate" }, { pbName: "Hypnotic Pattern", foundryName: "Hypnotize" }, { pbName: "Inspire Competence", foundryName: "Uplifting Overture" }, { pbName: "Inspire Courage", foundryName: "Courageous Anthem" }, { pbName: "Inspire Defense", foundryName: "Rallying Anthem" }, { pbName: "Inspire Heroics", foundryName: "Fortissimo Composition" }, { pbName: "Know Direction", foundryName: "Know the Way" }, { pbName: "Legend Lore", foundryName: "Collective Memories" }, { pbName: "Longstrider", foundryName: "Tailwind" }, { pbName: "Mage Armor", foundryName: "Mystic Armor" }, { pbName: "Mage Hand", foundryName: "Telekinetic Hand" }, { pbName: "Magic Aura", foundryName: "Disguise Magic" }, { pbName: "Magic Fang", foundryName: "Runic Body" }, { pbName: "Magic Missile", foundryName: "Force Barrage" }, { pbName: "Magic Mouth", foundryName: "Embed Message" }, { pbName: "Magic Weapon", foundryName: "Runic Weapon" }, { pbName: "Magnificent Mansion", foundryName: "Planar Palace" }, { pbName: "Maze", foundryName: "Quandary" }, { pbName: "Meld into Stone", foundryName: "One with Stone" }, { pbName: "Meteor Swarm", foundryName: "Falling Stars" }, { pbName: "Mind Blank", foundryName: "Hidden Mind" }, { pbName: "Misdirection", foundryName: "Disguise Magic" }, { pbName: "Modify Memory", foundryName: "Rewrite Memory" }, { pbName: "Neutralize Poison", foundryName: "Cleanse Affliction" }, { pbName: "Nondetection", foundryName: "Veil of Privacy" }, { pbName: "Obscuring Mist", foundryName: "Mist" }, { pbName: "Pass Without Trace", foundryName: "Vanishing Tracks" }, { pbName: "Passwall", foundryName: "Magic Passage" }, { pbName: "Phantom Mount", foundryName: "Marvelous Mount" }, { pbName: "Planar Binding", foundryName: "Planar Servitor" }, { pbName: "Plane Shift", foundryName: "Interplanar Teleport" }, { pbName: "Positive Luminance", foundryName: "Vital Luminance" }, { pbName: "Private Sanctum", foundryName: "Peaceful Bubble" }, { pbName: "Prying Eye", foundryName: "Scouting Eye" }, { pbName: "Pulse of The City", foundryName: "Pulse of Civilization" }, { pbName: "Purify Food And Drink", foundryName: "Cleanse Cuisine" }, { pbName: "Ray of Enfeeblement", foundryName: "Enfeeble" }, { pbName: "Remove Curse", foundryName: "Cleanse Affliction" }, { pbName: "Remove Disease", foundryName: "Cleanse Affliction" }, { pbName: "Remove Fear", foundryName: "Clear Mind" }, { pbName: "Remove Paralysis", foundryName: "Sure Footing" }, { pbName: "Restore Senses", foundryName: "Sound Body" }, { pbName: "Scorching Ray", foundryName: "Blazing Bolt" }, { pbName: "Searing Light", foundryName: "Holy Light" }, { pbName: "See Invisibility", foundryName: "See the Unseen" }, { pbName: "Shadow Walk", foundryName: "Umbral Journey" }, { pbName: "Shapechange", foundryName: "Metamorphosis" }, { pbName: "Shield Other", foundryName: "Share Life" }, { pbName: "Sound Burst", foundryName: "Noise Blast" }, { pbName: "Spectral Hand", foundryName: "Ghostly Carrier" }, { pbName: "Spider Climb", foundryName: "Gecko Grip" }, { pbName: "Splash of Art", foundryName: "Creative Splash" }, { pbName: "Stone Tell", foundryName: "Speak with Stones" }, { pbName: "Stoneskin", foundryName: "Mountain Resilience" }, { pbName: "Tanglefoot", foundryName: "Tangle Vine" }, { pbName: "Time Stop", foundryName: "Freeze Time" }, { pbName: "Tongues", foundryName: "Truespeech" }, { pbName: "Touch of Idiocy", foundryName: "Stupefy" }, { pbName: "Tree Shape", foundryName: "One with Plants" }, { pbName: "Tree Stride", foundryName: "Nature's Pathway" }, { pbName: "Trueseeing", foundryName: "Truesight" }, { pbName: "True Strike", foundryName: "Sure Strike" }, { pbName: "Unseen Servant", foundryName: "Phantasmal Minion" }, { pbName: "Vampiric Touch", foundryName: "Vampiric Feast" }, { pbName: "Veil", foundryName: "Illusory Disguise" }, { pbName: "Vigilant Eye", foundryName: "Rune of Observation" }, { pbName: "Wail of the Banshee", foundryName: "Wails of the Damned" }, { pbName: "Wind Walk", foundryName: "Migration" }, { pbName: "Zone of Truth", foundryName: "Ring of Truth" }, ]; function spellRename(spellName) { if (isNewerVersion(game.version, "5.9.0") && game.modules.get("pf2e-legacy-content")?.active) return spellName; const remasterName = REMASTER_NAMES.find((remaster) => remaster.pbName === spellName); if (remasterName) { return remasterName.foundryName; } return spellName; } ;// CONCATENATED MODULE: ./src/app/Seasoning.js /** * This class acts as a wrapper around the renaming data, * and the changing of names for foundry * * When Munching we refer to this as Seasoning the data to taste. * * It's split out just to make it more manageable */ class Seasoning { // sluggify static slug(name) { if (!name) return undefined; return game.pf2e.system.sluggify(name); } // sluggify with dromedary casing static slugD(name) { if (!name) return undefined; return game.pf2e.system.sluggify(name, { camel: "dromedary" }); } static FEAT_RENAME_MAP(name) { return FEAT_RENAME_MAP(name); } static EQUIPMENT_RENAME_MAP(name) { return EQUIPMENT_RENAME_MAP(name); } static getSpellCastingFeatureAdjustment(name) { return FEAT_SPELLCASTING.find((f) => f.name === name); } static getFoundryEquipmentName(pbName) { return Seasoning.EQUIPMENT_RENAME_MAP(pbName).find((map) => map.pbName == pbName)?.foundryName ?? pbName; } // static getFoundryFeatureName(pbName) { // const match = Seasoning.FEAT_RENAME_MAP(pbName).find((map) => map.pbName == pbName); // return match ?? { pbName, foundryName: pbName, details: undefined }; // } static RESTRICTED_EQUIPMENT() { return RESTRICTED_EQUIPMENT; } // specials that are handled by Foundry and shouldn't be added static IGNORED_FEATS() { return IGNORED_FEATS(); }; static IGNORED_SPECIALS() { return IGNORED_SPECIALS(); } static IGNORED_EQUIPMENT() { return IGNORED_EQUIPMENT; }; static IGNORED_EQUIPMENT_DISPLAY(pbName) { return IGNORED_EQUIPMENT_DISPLAY(pbName); } static GRANTED_ITEMS_LIST() { return GRANTED_ITEMS_LIST; } static getSizeValue(size) { switch (size) { case 0: return "tiny"; case 1: return "sm"; case 3: return "lg"; default: return "med"; } } static PHYSICAL_ITEM_TYPES = new Set([ "armor", "backpack", "book", "consumable", "equipment", "treasure", "weapon" ]); static isPhysicalItemType(type) { return Seasoning.PHYSICAL_ITEM_TYPES.has(type); } static getMaterialGrade(material) { if (material.toLowerCase().includes("high-grade")) { return "high"; } else if (material.toLowerCase().includes("standard-grade")) { return "standard"; } return "low"; } static getFoundryFeatLocation(pathbuilderFeatType, pathbuilderFeatLevel) { if (pathbuilderFeatType === "Ancestry Feat") { return `ancestry-${pathbuilderFeatLevel}`; } else if (pathbuilderFeatType === "Class Feat") { return `class-${pathbuilderFeatLevel}`; } else if (pathbuilderFeatType === "Skill Feat") { return `skill-${pathbuilderFeatLevel}`; } else if (pathbuilderFeatType === "General Feat") { return `general-${pathbuilderFeatLevel}`; } else if (pathbuilderFeatType === "Background Feat") { return `skill-${pathbuilderFeatLevel}`; } else if (pathbuilderFeatType === "Archetype Feat") { return `archetype-${pathbuilderFeatLevel}`; } else if (pathbuilderFeatType === "Kineticist Feat") { // return as null for now return null; } else { return null; } } static getClassAdjustedSpecialNameLowerCase(name, className) { return `${name} (${className})`.toLowerCase(); } static getDualClassAdjustedSpecialNameLowerCase(name, dualClassName) { return `${name} (${dualClassName})`.toLowerCase(); } static getAncestryAdjustedSpecialNameLowerCase(name, ancestryName) { return `${name} (${ancestryName})`.toLowerCase(); } static getHeritageAdjustedSpecialNameLowerCase(name, heritageName) { return `${name} (${heritageName})`.toLowerCase(); } static getChampionType(alignment) { if (alignment == "LG") return "Paladin"; else if (alignment == "CG") return "Liberator"; else if (alignment == "NG") return "Redeemer"; else if (alignment == "LE") return "Tyrant"; else if (alignment == "CE") return "Antipaladin"; else if (alignment == "NE") return "Desecrator"; return "Unknown"; } } ;// CONCATENATED MODULE: ./src/app/CompendiumMatcher.js /* eslint-disable no-await-in-loop */ class CompendiumMatcher { constructor({ type, mappings = null, indexFields = ["name", "type", "system.slug"] } = {}) { this.type = type; this.indexFields = indexFields; this.packs = {}; const packMappings = mappings !== null ? mappings : src_utils.setting("USE_CUSTOM_COMPENDIUM_MAPPINGS") ? src_utils.setting("CUSTOM_COMPENDIUM_MAPPINGS") : constants.CORE_COMPENDIUM_MAPPINGS; packMappings[type].forEach((name) => { const compendium = game.packs.get(name); if (compendium) { this.packs[name] = compendium; } }); this.indexes = { }; } async loadCompendiums() { for (const [name, compendium] of Object.entries(this.packs)) { this.indexes[name] = await compendium.getIndex({ fields: this.indexFields }); } } getFoundryFeatureName(pbName) { const match = this.FEAT_RENAME_MAP(pbName).find((map) => map.pbName == pbName); return match ?? { pbName, foundryName: pbName, details: undefined }; } getNameMatch(pbName, foundryName) { for (const [packName, index] of Object.entries(this.indexes)) { const indexMatch = index.find((i) => i.name === foundryName) ?? index.find((i) => i.name === pbName); if (indexMatch) { src_logger.debug(`Found name only compendium document for ${pbName} (${foundryName}) in ${packName} with id ${indexMatch._id}`); return { i: indexMatch, pack: this.packs[packName] }; } } return undefined; } getSlugMatch(pbName, foundryName) { for (const [packName, index] of Object.entries(this.indexes)) { src_logger.debug(`Checking for compendium documents for ${pbName} (${foundryName}) in ${packName}`, { pbName, foundryName, packName, // index, // foundrySlug: Seasoning.slug(foundryName), // pbSlug: Seasoning.slug(pbName), // foundryMatch: index.find((i) => (i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(foundryName)), // pbMatch: index.find((i) => (i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(pbName)), // pbSlugMatch: (null ?? Seasoning.slug("Phase Bolt (Psychic)")) === Seasoning.slug("Phase Bolt (Psychic)"), }); const indexMatch = index.find((i) => (i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(foundryName)) ?? index.find((i) => (i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(pbName)); if (indexMatch) { src_logger.debug(`Found slug based compendium document for ${pbName} (${foundryName}) in ${packName} with id ${indexMatch._id}`); return { i: indexMatch, pack: this.packs[packName] }; } } return undefined; } getMatch(pbName, foundryName, forceName = false) { if (forceName) { const nameOnlyMatch = this.getNameMatch(pbName, foundryName); if (nameOnlyMatch) return nameOnlyMatch; } const slugMatch = this.getSlugMatch(pbName, foundryName); if (slugMatch) return slugMatch; return undefined; } static checkForFilters(i, filters) { for (const [key, value] of Object.entries(filters)) { if (getProperty(i, key) !== value) { return false; } } return true; } getNameMatchWithFilter(pbName, foundryName, filters = {}) { for (const [packName, index] of Object.entries(this.indexes)) { src_logger.debug(`Checking for compendium documents for ${pbName} (${foundryName}) in ${packName}`, { pbName, foundryName, filters, packName, // index, }); const indexMatch = index.find((i) => ((i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(foundryName)) && CompendiumMatcher.checkForFilters(i, filters)) ?? index.find((i) => ((i.system.slug ?? Seasoning.slug(i.name)) === Seasoning.slug(pbName) && CompendiumMatcher.checkForFilters(i, filters)) ); if (indexMatch) { src_logger.debug(`Found compendium document for ${pbName} (${foundryName}) in ${packName} with id ${indexMatch._id}`); return { i: indexMatch, pack: this.packs[packName] }; } } return undefined; } } ;// CONCATENATED MODULE: ./src/app/CompendiumSelector.js class CompendiumSelector extends FormApplication { constructor() { super(); this.lookups = src_utils.setting("CUSTOM_COMPENDIUM_MAPPINGS"); this.packs = game.packs .filter((p) => p.metadata.type === "Item") .map((p) => { return { id: p.metadata.id, label: `${p.metadata.label} (${p.metadata.packageName})` }; }); this.currentType = null; this.useCustomCompendiums = src_utils.setting("USE_CUSTOM_COMPENDIUM_MAPPINGS"); } static get defaultOptions() { return mergeObject(super.defaultOptions, { id: "pathmuncher-compendium-selector", template: `${constants.PATH}/templates/compendium-selector.hbs`, width: 722, height: 275, title: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.CompendiumSelector.Title`), resizable: true, classes: ['pathmuncher-compendium-selector'], }); } getData() { const lookups = []; for (const key in this.lookups) { lookups.push({ key, label: game.i18n.localize(`${constants.FLAG_NAME}.CompendiumGroups.${key}`), }); } return { lookups, title: this.options.title, sourceItems: [], compendiumItems: [], useCustomCompendiums: this.useCustomCompendiums, }; } async reset() { const defaults = constants.GET_DEFAULT_SETTINGS(); this.lookups = defaults[constants.SETTINGS.CUSTOM_COMPENDIUM_MAPPINGS].default; await src_utils.updateSetting("CUSTOM_COMPENDIUM_MAPPINGS", this.lookups); this.currentType = null; this.render(true); } async enableCustomCompendiums() { this.useCustomCompendiums = !this.useCustomCompendiums; await src_utils.updateSetting("USE_CUSTOM_COMPENDIUM_MAPPINGS", this.useCustomCompendiums); } filterList(event) { const compendiumType = event.srcElement.value; const sourceList = document.getElementById("sourceList"); const compendiumList = document.getElementById("compendiumList"); const sourceOptions = this.packs.filter((p) => !this.lookups[compendiumType].includes(p.id)); const compendiumOptions = this.packs.filter((p) => this.lookups[compendiumType].includes(p.id)); sourceList.innerHTML = ""; compendiumList.innerHTML = ""; sourceOptions.forEach((option) => { const sourceListItem = document.createElement("option"); sourceListItem.value = option.id; sourceListItem.appendChild(document.createTextNode(option.label)); sourceList.appendChild(sourceListItem); }); compendiumOptions.forEach((option) => { const compendiumListItem = document.createElement("option"); compendiumListItem.value = option.id; compendiumListItem.appendChild(document.createTextNode(option.label)); compendiumList.appendChild(compendiumListItem); }); this.currentType = compendiumType; } async updateCompendiums() { const compendiumList = document.getElementById("compendiumList"); const compendiumOptions = Array.from(compendiumList.options); const compendiumIds = compendiumOptions.map((option) => { return option.value; }); this.lookups[this.currentType] = compendiumIds; src_utils.updateSetting("CUSTOM_COMPENDIUM_MAPPINGS", this.lookups); } async addCompendium() { const sourceList = document.getElementById("sourceList"); const compendiumList = document.getElementById("compendiumList"); const selectedOptions = Array.from(sourceList.selectedOptions); selectedOptions.forEach((option) => { compendiumList.appendChild(option); }); await this.updateCompendiums(); } async removeCompendium() { const sourceList = document.getElementById("sourceList"); const compendiumList = document.getElementById("compendiumList"); const selectedOptions = Array.from(compendiumList.selectedOptions); selectedOptions.forEach((option) => { sourceList.appendChild(option); }); await this.updateCompendiums(); } async moveUp() { const compendiumList = document.getElementById("compendiumList"); const selectedOption = compendiumList.selectedOptions[0]; if (selectedOption && selectedOption.previousElementSibling) { compendiumList.insertBefore(selectedOption, selectedOption.previousElementSibling); } await this.updateCompendiums(); } async moveDown() { const compendiumList = document.getElementById("compendiumList"); const selectedOption = compendiumList.selectedOptions[0]; if (selectedOption && selectedOption.nextElementSibling) { compendiumList.insertBefore(selectedOption.nextElementSibling, selectedOption); } await this.updateCompendiums(); } activateListeners(html) { super.activateListeners(html); document.getElementById("addButton").addEventListener("click", this.addCompendium.bind(this)); document.getElementById("removeButton").addEventListener("click", this.removeCompendium.bind(this)); document.getElementById("upButton").addEventListener("click", this.moveUp.bind(this)); document.getElementById("downButton").addEventListener("click", this.moveDown.bind(this)); document.getElementById("compSelector").addEventListener("change", this.filterList.bind(this)); document.getElementById("resetButton").addEventListener("click", this.reset.bind(this)); document.getElementById("enableCustomCompendiums").addEventListener("change", this.enableCustomCompendiums.bind(this)); } } ;// CONCATENATED MODULE: ./src/app/Pathmuncher.js /* eslint-disable no-await-in-loop */ /* eslint-disable no-continue */ class Pathmuncher { FEAT_RENAME_MAP(name) { const dynamicItems = [ { pbName: "Shining Oath", foundryName: `Shining Oath (${Seasoning.getChampionType(this.source.alignment)})` }, { pbName: "Counterspell", foundryName: `Counterspell (${src_utils.capitalize(this.getClassSpellCastingType() ?? "")})` }, { pbName: "Counterspell", foundryName: `Counterspell (${src_utils.capitalize(this.getClassSpellCastingType(true) ?? "")})` }, { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${this.source.class})` }, { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${this.source.dualClass})` }, { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${src_utils.capitalize(this.getClassSpellCastingType() ?? "")} Caster)` }, { pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${src_utils.capitalize(this.getClassSpellCastingType(true) ?? "")} Caster)` }, ]; return Seasoning.FEAT_RENAME_MAP(name).concat(dynamicItems); } getFoundryFeatureName(pbName, isSpecial = false) { if (isSpecial) { const specialMatch = specialOnlyNameLookup(pbName); if (specialMatch) return specialMatch; } const match = this.FEAT_RENAME_MAP(pbName).find((map) => map.pbName == pbName); return match ?? { pbName, foundryName: pbName, details: undefined }; } constructor(actor, { addFeats = true, addEquipment = true, addSpells = true, adjustBlendedSlots = true, addMoney = true, addLores = true, addWeapons = true, addArmor = true, addTreasure = true, addDeity = true, addName = true, addClass = true, addBackground = true, addHeritage = true, addAncestry = true, statusCallback = null } = {} ) { this.devMode = game.modules.get("pathmuncher").version === "999.0.0"; this.actor = actor; // note not all these options do anything yet! this.options = { addTreasure, addMoney, addFeats, addSpells, adjustBlendedSlots, addEquipment, addLores, addWeapons, addArmor, addDeity, addName, addClass, addBackground, addHeritage, addAncestry, }; this.source = null; this.parsed = { specials: [], feats: [], equipment: [], armor: [], weapons: [], }; this.usedLocations = new Set(); this.usedLocationsAlternateRules = new Set(); this.autoAddedFeatureIds = new Set(); this.autoAddedFeatureItems = {}; this.promptRules = {}; this.allFeatureRules = {}; this.autoAddedFeatureRules = {}; this.grantItemLookUp = {}; this.autoFeats = []; this.keyAbility = null; this.boosts = { custom: false, class: {}, background: {}, ancestry: {}, }; this.size = "med"; this.result = { character: { _id: this.actor.id, prototypeToken: {}, }, class: [], deity: [], heritage: [], ancestry: [], background: [], casters: [], spells: [], feats: [], weapons: [], armor: [], equipment: [], lores: [], money: [], treasure: [], adventurersPack: { item: null, contents: [ { slug: "bedroll", qty: 1 }, { slug: "chalk", qty: 10 }, { slug: "flint-and-steel", qty: 1 }, { slug: "rope", qty: 1 }, { slug: "rations", qty: 14 }, { slug: "torch", qty: 5 }, { slug: "waterskin", qty: 1 }, ], }, focusPool: 0, }; this.check = {}; this.bad = []; this.statusCallback = statusCallback; this.compendiumMatchers = {}; const compendiumMappings = src_utils.setting("USE_CUSTOM_COMPENDIUM_MAPPINGS") ? src_utils.setting("CUSTOM_COMPENDIUM_MAPPINGS") : constants.CORE_COMPENDIUM_MAPPINGS; for (const type of Object.keys(compendiumMappings)) { this.compendiumMatchers[type] = new CompendiumMatcher({ type }); } } async #loadCompendiumMatchers() { for (const matcher of Object.values(this.compendiumMatchers)) { await matcher.loadCompendiums(); } } #statusUpdate(total, count, type, prefixLabel) { if (this.statusCallback) this.statusCallback(total, count, type, prefixLabel); } async fetchPathbuilder(pathbuilderId) { if (!pathbuilderId) { const flags = src_utils.getFlags(this.actor); pathbuilderId = flags?.pathbuilderId; } if (pathbuilderId) { const jsonData = await foundry.utils.fetchJsonWithTimeout( `https://www.pathbuilder2e.com/json.php?id=${pathbuilderId}` ); if (jsonData.success) { this.source = jsonData.build; } else { ui.notifications.warn( game.i18n.format(`${constants.FLAG_NAME}.Dialogs.Pathmuncher.FetchFailed`, { pathbuilderId }) ); } } else { ui.notifications.error(game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.Pathmuncher.NoId`)); } } #generateFoundryFeatLocation(document, feature) { if (feature.type && feature.level) { // const freeArchetypeVariant = game.settings.get("pf2e", "freeArchetypeVariant"); const location = Seasoning.getFoundryFeatLocation(feature.type, feature.level); if (location && !this.usedLocations.has(location)) { document.system.location = location; this.usedLocations.add(location); } else if (location && this.usedLocations.has(location)) { src_logger.debug("Variant feat location", { ancestryParagonVariant: src_utils.allowAncestryParagon(), location, feature }); // eslint-disable-next-line max-depth if (src_utils.allowAncestryParagon() && feature.type === "Ancestry Feat") { document.system.location = "ancestry-bonus"; this.usedLocationsAlternateRules.add(location); } else if (src_utils.allowDualClasses() && feature.type === "Class Feat") { document.system.location = `dualclass-${feature.level}`; this.usedLocationsAlternateRules.add(location); } } } } #processSpecialData(name) { if (name.includes("Domain: ")) { const domainName = name.split(" ")[1]; this.parsed.feats.push({ name: "Deity's Domain", extra: domainName }); return true; } else { return false; } } #getContainerData(key) { return { id: key, containerName: this.source.equipmentContainers[key].containerName, bagOfHolding: this.source.equipmentContainers[key].bagOfHolding, backpack: this.source.equipmentContainers[key].backpack, }; } #nameMapSourceEquipment(e) { const name = Seasoning.getFoundryEquipmentName(e[0]); const containerKey = Object.keys(this.source.equipmentContainers) .find((key) => this.source.equipmentContainers[key].containerName === name); const container = containerKey ? this.#getContainerData(containerKey) : null; const foundryId = foundry.utils.randomID(); if (container) { this.source.equipmentContainers[containerKey].foundryId = foundryId; } const item = { foundryName: name, pbName: e[0], originalName: e[0], qty: e[1], added: false, addedId: null, addedAutoId: null, inContainer: e[2] !== "Invested" ? e[2] : null, container, foundryId, invested: e[2] === "Invested", sourceType: "equipment", }; this.parsed.equipment.push(item); } #nameMapSourceEquipmentAddHandwraps(e) { const name = Seasoning.getFoundryEquipmentName(e[0]); const potencyMatch = e[0].match(/\(\+(\d)[\s)]/i); const potency = potencyMatch ? parseInt(potencyMatch[1]) : 0; const strikingMatch = e[0].match(/\d( \w*)? (Striking)/i); const striking = strikingMatch ? Seasoning.slugD(`${(strikingMatch[1] ?? "").trim()}${(strikingMatch[2] ?? "").trim()}`) // `${(strikingMatch[2] ?? "").toLowerCase().trim()}${(strikingMatch[1] ?? "").trim()}`.trim() : ""; const mockE = { name: e[0], qty: 1, prof: "unarmed", pot: Number.isInteger(potency) ? potency : 0, str: striking, mat: null, display: e[0], runes: [], damageType: "B", increasedDice: false }; const weapon = mergeObject({ foundryName: name, pbName: mockE.name, originalName: mockE.name, added: false, addedId: null, addedAutoId: null, sourceType: "weapons", }, mockE); this.parsed.weapons.push(weapon); } #nameMap() { let iRank = 0; let featRank = 0; src_logger.debug("Starting Equipment Rename"); this.source.equipment .filter((e) => e[0] && e[0] !== "undefined") .forEach((e) => { if (e[0].startsWith("Handwraps of Mighty Blows")) { this.#nameMapSourceEquipmentAddHandwraps(e); } else { this.#nameMapSourceEquipment(e); } }); this.source.armor .filter((e) => e && e !== "undefined") .forEach((e) => { const name = Seasoning.getFoundryEquipmentName(e.name); const item = mergeObject({ foundryName: name, pbName: e.name, originalName: e.name, added: false, addedId: null, addedAutoId: null, sourceType: "armor", }, e); this.parsed.armor.push(item); // work around for now if (e.name.startsWith("Inventor ")) { this.parsed.feats.push({ name, extra: "", added: false, addedId: null, addedAutoId: null, type: "Awarded Feat", level: 1, originalName: e.name, rank: 0, sourceType: "armor", featChoiceRef: null, hasChildren: null, isChild: null, isStandard: null, parentFeatChoiceRef: null, }); featRank++; } }); this.source.weapons .filter((e) => e && e !== "undefined") .forEach((e) => { const name = Seasoning.getFoundryEquipmentName(e.name); const item = mergeObject({ foundryName: name, pbName: e.name, originalName: e.name, added: false, addedId: null, addedAutoId: null, sourceType: "weapons", }, e); this.parsed.weapons.push(item); }); src_logger.debug("Finished Equipment Rename"); src_logger.debug("Starting Special Rename"); [].concat(this.source.specials, SPECIAL_NAME_ADDITIONS(this.source.specials)) .filter((special) => special && special !== "undefined" && special !== "Not Selected" && special !== this.source.heritage ) .forEach((special) => { const match = this.getFoundryFeatureName(special); // , true); if (!this.#processSpecialData(match.foundryName) && !Seasoning.IGNORED_SPECIALS().includes(match.foundryName)) { this.parsed.specials.push({ name: match.foundryName, foundryName: match.foundryName, foundryValue: match.foundryValue, originalName: special, added: false, addedId: null, addedAutoId: null, rank: iRank, sourceType: "specials" }); iRank++; } }); src_logger.debug("Finished Special Rename"); src_logger.debug("Starting Feat Rename"); this.source.feats .filter((feat) => feat[0] && feat[0] !== "undefined" && feat[0] !== "Not Selected" // && feat[0] !== this.source.heritage ) .forEach((feat) => { const name = this.getFoundryFeatureName(feat[0]).foundryName; const data = { name, extra: feat[1], added: feat[0] === this.source.heritage, addedId: null, addedAutoId: null, type: feat[2], level: feat[3], originalName: feat[0], rank: featRank, sourceType: "feats", }; if (feat.length >= 7) { data.featChoiceRef = feat[4]; data.hasChildren = feat[5] === "parentChoice"; data.isChild = feat[5] === "childChoice"; data.isStandard = feat[5] === "standardChoice"; data.parentFeatChoiceRef = feat[6]; const parentFeatMatch = this.source.feats.find((f) => feat[5] === "childChoice" && (data.featChoiceRef.toLowerCase().startsWith(f[0].toLowerCase()) || (data.parentFeatChoiceRef && data.featChoiceRef.replace(data.parentFeatChoiceRef, "").trim().toLowerCase().startsWith(f[0].toLowerCase())) ) ); data.nameHint = parentFeatMatch?.[0]; } else { // probably an awarded feat data.featChoiceRef = null; data.hasChildren = null; data.isChild = null; data.isStandard = null; data.parentFeatChoiceRef = null; } this.parsed.feats.push(data); featRank++; }); src_logger.debug("Finished Feat Rename"); src_logger.debug("Name remapping results", { parsed: this.parsed, }); } #fixUps() { if (this.source.ancestry === "Dwarf" && !this.parsed.feats.some((f) => f.name === "Clan Pistol")) { const clanDagger = { name: "Clan Dagger", originalName: "Clan Dagger", added: false, addedId: null, addedAutoId: null, isChoice: true, rank: 0, sourceType: "specials", }; this.parsed.specials.push(clanDagger); } const match = this.source.background.match(/(Magical Experiment) \((.*)\)$/); if (match) { this.parsed.specials.push({ name: match[2], originalName: `${this.source.background}`, added: false, addedId: null, addedAutoId: null, isChoice: true, rank: 0, sourceType: "specials", }); this.source.background = match[1]; } } async #prepare() { await this.#loadCompendiumMatchers(); this.#nameMap(); this.#fixUps(); } async #processSenses() { const senses = []; this.source.specials.forEach((special) => { if (special === "Low-Light Vision") { senses.push({ type: "lowLightVision" }); } else if (special === "Darkvision") { senses.push({ type: "darkvision" }); } else if (special === "Scent") { senses.push({ type: "scent" }); } }); setProperty(this.result.character, "system.traits.senses", senses); } // eslint-disable-next-line no-unused-vars async #addDualClass(klass) { if (!src_utils.allowDualClasses()) { if (this.source.dualClass && this.source.dualClass !== "") { src_logger.warn(`Imported character is dual class. Pathmuncher does not support dual class characters, please check the system macros`, { class: this.source.class, dualClass: this.source.dualClass, }); ui.notifications.warn(`Imported character is dual class. Pathmuncher does not support dual class characters, please check the system macros`); } return; } if (!this.source.dualClass || this.source.dualClass === "") { src_logger.warn(`Imported character not dual class but system is configured for dual class`, { class: this.source.class, }); ui.notifications.warn(`Imported character not dual class but system is configured for dual class`); return; } src_logger.info("Not processing dual class"); // // find the dual class // const foundryName = this.getFoundryFeatureName(this.source.dualClass).foundryName; // const indexMatch = this.compendiumMatchers["classes"].getMatch(this.source.dualClass, foundryName); // if (!indexMatch) return; // const doc = await indexMatch.pack.getDocument(indexMatch.i._id); // const dualClass = doc.toObject(); // logger.debug(`Dual Class ${dualClass.name} found, squashing things together.`); // klass.name = `${klass.name} - ${dualClass.name}`; // const ruleEntry = { // domain: "all", // key: "RollOption", // option: `class:${dualClass.system.slug}`, // }; // // Attacks // ["advanced", "martial", "simple", "unarmed"].forEach((key) => { // if (dualClass.system.attacks[key] > klass.system.attacks[key]) { // klass.system.attacks[key] = dualClass.system.attacks[key]; // } // }); // if (klass.system.attacks.martial <= dualClass.system.attacks.other.rank) { // if (dualClass.system.attacks.other.rank === klass.system.attacks.other.rank) { // let mashed = `${klass.system.attacks.other.name}, ${dualClass.system.attacks.other.name}`; // mashed = mashed.replace("and ", ""); // klass.system.attacks.other.name = [...new Set(mashed.split(","))].join(","); // } // if (dualClass.system.attacks.other.rank > klass.system.attacks.other.rank) { // klass.system.attacks.other.name = dualClass.system.attacks.other.name; // klass.system.attacks.other.rank = dualClass.system.attacks.other.rank; // } // } // if ( // klass.system.attacks.martial >= dualClass.system.attacks.other.rank // && klass.system.attacks.martial >= klass.system.attacks.other.rank // ) { // klass.system.attacks.other.rank = 0; // klass.system.attacks.other.name = ""; // } // // Class DC // if (dualClass.system.classDC > klass.system.classDC) { // klass.system.classDC = dualClass.system.classDC; // } // // Defenses // ["heavy", "light", "medium", "unarmored"].forEach((key) => { // if (dualClass.system.defenses[key] > klass.system.defenses[key]) { // klass.system.defenses[key] = dualClass.system.defenses[key]; // } // }); // // Description // klass.system.description.value = `${klass.system.description.value} ${dualClass.system.description.value}`; // // HP // if (dualClass.system.hp > klass.system.hp) { // klass.system.hp = dualClass.system.hp; // } // // Items // Object.entries(dualClass.system.items).forEach((i) => { // if (Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level > i[1].level)) { // Object.values(klass.system.items).find((x) => x.uuid === i[1].uuid).level = i[1].level; // } else if (!Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level <= i[1].level)) { // klass.system.items[i[0]] = i[1]; // } // }); // // Key Ability // dualClass.system.keyAbility.value.forEach((v) => { // if (!klass.system.keyAbility.value.includes(v)) { // klass.system.keyAbility.value.push(v); // } // }); // // Perception // if (dualClass.system.perception > klass.system.perception) klass.system.perception = dualClass.system.perception; // // Rules // klass.system.rules.push(ruleEntry); // dualClass.system.rules.forEach((r) => { // if (!klass.system.rules.includes(r)) { // klass.system.rules.push(r); // } // }); // klass.system.rules.forEach((r, i) => { // if (r.path !== undefined) { // const check = r.path.split("."); // if ( // check.includes("data") // && check.includes("martial") // && check.includes("rank") // && klass.system.attacks.martial >= r.value // ) { // klass.system.rules.splice(i, 1); // } // } // }); // // Saving Throws // ["fortitude", "reflex", "will"].forEach((key) => { // if (dualClass.system.savingThrows[key] > klass.system.savingThrows[key]) { // klass.system.savingThrows[key] = dualClass.system.savingThrows[key]; // } // }); // // Skill Feat Levels // dualClass.system.skillFeatLevels.value.forEach((v) => { // klass.system.skillFeatLevels.value.push(v); // }); // klass.system.skillFeatLevels.value = [...new Set(klass.system.skillFeatLevels.value)].sort((a, b) => { // return a - b; // }); // // Skill Increase Levels // dualClass.system.skillIncreaseLevels.value.forEach((v) => { // klass.system.skillIncreaseLevels.value.push(v); // }); // klass.system.skillIncreaseLevels.value = [...new Set(klass.system.skillIncreaseLevels.value)].sort((a, b) => { // return a - b; // }); // // Trained Skills // if (dualClass.system.trainedSkills.additional > klass.system.trainedSkills.additional) { // klass.system.trainedSkills.additional = dualClass.system.trainedSkills.additional; // } // dualClass.system.trainedSkills.value.forEach((v) => { // if (!klass.system.trainedSkills.value.includes(v)) { // klass.system.trainedSkills.value.push(v); // } // }); // this.result.dualClass = dualClass; } // eslint-disable-next-line class-methods-use-this async #processGenericCompendiumLookup(type, name, target) { src_logger.debug(`Checking for compendium documents for ${name} (${target}) in compendiums for ${type}`); const foundryName = this.getFoundryFeatureName(name).foundryName; const indexMatch = this.compendiumMatchers[type].getMatch(name, foundryName); if (indexMatch) { const doc = await indexMatch.pack.getDocument(indexMatch.i._id); const itemData = doc.toObject(); if (name.includes("(")) { const extra = name.split(")")[0].split("(").pop(); this.parsed.specials.push({ name: doc.name, originalName: name, added: true, extra, rank: 99 }); } if (target === "class") { itemData.system.keyAbility.selected = this.keyAbility; await this.#addDualClass(itemData); } itemData._id = foundry.utils.randomID(); // this.#generateGrantItemData(itemData); this.result[target].push(itemData); await this.#addGrantedItems(itemData, { applyFeatLocation: target !== "class" }); return true; } else { this.bad.push({ pbName: name, type: target, details: { name } }); return false; } } // for grants, e.g. ont he champion "Deity and Cause" where there are choices. // how do we determine and match these? should we? // "pf2e": { // "itemGrants": { // "adanye": { // "id": "4GHcp3iaREfj2ZgN", // "onDelete": "detach" // }, // "paladin": { // "id": "HGWkTEatliHgDaEu", // "onDelete": "detach" // } // } // } // "Paladin" (granted by deity and casue) // "pf2e": { // "grantedBy": { // "id": "xnrkrJa2YE1UOAVy", // "onDelete": "cascade" // }, // "itemGrants": { // "retributiveStrike": { // "id": "WVHbj9LljCTovdsv", // "onDelete": "detach" // } // } // } // retributive strike // "pf2e": { // "grantedBy": { // "id": "HGWkTEatliHgDaEu", // "onDelete": "cascade" // } #slugNameMatch(f, slug) { return slug === Seasoning.slug(f.name) || slug === Seasoning.slug(f.foundryValue) || slug === Seasoning.slug(Seasoning.getClassAdjustedSpecialNameLowerCase(f.name, this.source.class)) || slug === Seasoning.slug(Seasoning.getAncestryAdjustedSpecialNameLowerCase(f.name, this.source.ancestry)) || slug === Seasoning.slug(Seasoning.getHeritageAdjustedSpecialNameLowerCase(f.name, this.source.heritage)) || slug === Seasoning.slug(f.originalName) || slug === Seasoning.slug(Seasoning.getClassAdjustedSpecialNameLowerCase(f.originalName, this.source.class)) || slug === Seasoning.slug(Seasoning.getAncestryAdjustedSpecialNameLowerCase(f.originalName, this.source.ancestry)) || slug === Seasoning.slug(Seasoning.getHeritageAdjustedSpecialNameLowerCase(f.originalName, this.source.heritage)) || (src_utils.allowDualClasses() && (slug === Seasoning.slug(Seasoning.getDualClassAdjustedSpecialNameLowerCase(f.name, this.source.dualClass)) || slug === Seasoning.slug( Seasoning.getDualClassAdjustedSpecialNameLowerCase(f.originalName, this.source.dualClass) ))); } #parsedFeatureMatch(type, document, slug, { ignoreAdded, isChoiceMatch = false, featType = null } = {}) { if (type === "feats" && document) { const hintMatch = this.parsed[type].find((f) => (!ignoreAdded || (ignoreAdded && !f.added)) && f.isChild && f.nameHint && Seasoning.slug(document.name) === Seasoning.slug(f.nameHint) && this.#slugNameMatch(f, slug) ); if (hintMatch) { hintMatch.rank = -10; return hintMatch; } } // console.warn(`Trying to find ${slug} in ${type}, ignoreAdded? ${ignoreAdded}`, this.parsed[type]); const parsedMatch = this.parsed[type].find((f) => (!ignoreAdded || (ignoreAdded && !f.added)) && ( featType === null || f.type === featType ) && !f.isChoice && this.#slugNameMatch(f, slug) ); if (parsedMatch || !document) return parsedMatch; const extraMatch = this.parsed[type].find((f) => // (!ignoreAdded || (ignoreAdded && !f.added)) f.extra && f.added && !f.isChoice && Seasoning.slug(f.name) === (document.system.slug ?? Seasoning.slug(document.name)) && Seasoning.slug(f.extra) === slug ); if (extraMatch) return extraMatch; if (isChoiceMatch) { // console.warn("Specials check", { // document, // type, // slug, // }); const choiceMatch = this.parsed[type].find((f) => f.isChoice && !f.added && Seasoning.slug(f.name) === slug); return choiceMatch; } return undefined; } #generatedResultMatch(type, slug) { const featMatch = this.result[type].find((f) => slug === f.system.slug); return featMatch; } #findAllFeatureMatch(document, slug, { ignoreAdded, isChoiceMatch = false, featType = null } = {}) { // console.warn("Finding all feature matches", { document, slug, ignoreAdded, isChoiceMatch, featType }); const featMatch = this.#parsedFeatureMatch("feats", document, slug, { ignoreAdded, featType }); if (featMatch) return featMatch; const specialMatch = this.#parsedFeatureMatch("specials", document, slug, { ignoreAdded, isChoiceMatch }); if (specialMatch) return specialMatch; const deityMatch = this.#generatedResultMatch("deity", slug); return deityMatch; // const classMatch = this.#generatedResultMatch("class", slug); // return classMatch; // const equipmentMatch = this.#generatedResultMatch("equipment", slug); // return equipmentMatch; } #createGrantedItem(document, parent, { itemGrantName = null, originType = null, applyFeatLocation = false } = {}) { src_logger.debug(`Adding granted item flags to ${document.name} (parent ${parent.name}) with originType "${originType}", and will applyFeatLocation? ${applyFeatLocation}`); if (itemGrantName) { const camelCase = Seasoning.slugD(itemGrantName ?? document.system.slug ?? document.name); setProperty(parent, `flags.pf2e.itemGrants.${camelCase}`, { id: document._id, onDelete: "detach" }); setProperty(document, "flags.pf2e.grantedBy", { id: parent._id, onDelete: "cascade" }); } this.autoFeats.push(document); this.result.feats.push(document); const matchOptions = { ignoreAdded: true, featType: originType }; const featureMatch = this.#findAllFeatureMatch(document, document.system.slug ?? Seasoning.slug(document.name), matchOptions) ?? (document.name.includes("(") ? this.#findAllFeatureMatch(document, Seasoning.slug(document.name.split("(")[0].trim()), matchOptions) : undefined); if (featureMatch) { src_logger.debug(`Found feature match for ${document.name}`, { featureMatch }); const existingMatch = false; // featureMatch.sourceType // ? this.parsed[featureMatch.sourceType].some((f) => f.addedId === document._id) // : false; if (this.devMode && existingMatch) { src_logger.warn(`create Granted Item Existing match for ${document.name}`, { featureMatch, existingMatch, document }); } // console.warn(`Match for ${document.name} createGrantedItem`, { featureMatch, existingMatch, document }); if (hasProperty(featureMatch, "added") && !existingMatch) { featureMatch.added = true; featureMatch.addedId = document._id; if (applyFeatLocation) this.#generateFoundryFeatLocation(document, featureMatch); } return; } if (document.type !== "action") src_logger.warn( `Unable to find parsed feature match for granted feature ${document.name}. This might not be an issue, but might indicate feature duplication.`, { document, parent } ); } static #getLowestChoiceRank(choices) { return choices.reduce((p, c) => { return p.rank > c.rank ? c : p; }); } async #featureChoiceMatch(document, choices, ignoreAdded, adjustName, choiceHint = null) { const matches = []; for (const choice of choices) { const doc = adjustName ? game.i18n.localize(choice.label) : await fromUuid(choice.value); if (!doc) continue; const slug = adjustName ? Seasoning.slug(doc) : doc.system.slug === null ? Seasoning.slug(doc.name) : doc.system.slug; const featMatch = this.#findAllFeatureMatch(document, slug, { ignoreAdded, isChoiceMatch: false }); if (featMatch) { matches.push({ slug, rank: featMatch.rank, choice, featMatch, }); } } if (matches.length > 0) { if (choiceHint) { const hintMatch = matches.find((m) => m.slug === Seasoning.slug(choiceHint)); if (hintMatch) return hintMatch; } if (this.devMode) src_logger.warn(`MATCHES`, matches); const match = Pathmuncher.#getLowestChoiceRank(matches); const featMatch = this.#findAllFeatureMatch(document, match.slug, { ignoreAdded }); const existingMatch = false; // featMatch.sourceType // ? this.parsed[featMatch.sourceType].some((f) => f.addedId === document._id) // : false; if (this.devMode && existingMatch) { src_logger.warn(`Feature Choice Existing match for ${document.name}`, { featMatch, existingMatch, document }); } // console.warn(`Match for ${document.name} featureChoiceMatch`, { match, featMatch, existingMatch, document }); if (adjustName && hasProperty(featMatch, "added") && !existingMatch) { featMatch.added = true; featMatch.addedId = document._id; } src_logger.debug("Choices evaluated", { choices, document, featMatch, match, matches }); return match.choice; } else { return undefined; } } async #featureChoiceMatchNoUUID(document, choices, cleansedChoiceSet) { const matches = []; for (const choice of choices) { const featMatch = this.#findAllFeatureMatch(document, choice.value, { ignoreAdded: true, isChoiceMatch: true }); if (featMatch) { matches.push({ rank: featMatch.rank, choice, }); } } if (matches.length > 0) { const match = Pathmuncher.#getLowestChoiceRank(matches); const featMatch = this.#findAllFeatureMatch(document, match.choice.value, { ignoreAdded: true, isChoiceMatch: true }); const existingMatch = false; // featMatch.sourceType // ? this.parsed[featMatch.sourceType].some((f) => f.addedId === document._id) // : false; if (this.devMode && existingMatch) { src_logger.warn(`NoUUID Existing match for ${document.name}`, { featMatch, existingMatch, document }); } // console.warn(`Match for ${document.name} featureChoiceMatchNoUUID`, { match, featMatch, existingMatch, document }); if (featMatch && !existingMatch) { featMatch.added = true; featMatch.addedId = document._id; match.choice.nouuid = true; } src_logger.debug("No UUID Choices evaluated", { choices, cleansedChoiceSet, document, featMatch, match, matches }); return match.choice; } else { return undefined; } } static getFlag(document, ruleSet) { return typeof ruleSet.flag === "string" && ruleSet.flag.length > 0 ? ruleSet.flag.replace(/[^-a-z0-9]/gi, "") : Seasoning.slugD(document.system.slug ?? document.system.name ?? document.name); } async #evaluateChoices(document, choiceSet, choiceHint) { src_logger.debug(`Evaluating choices for ${document.name}`, { document, choiceSet, choiceHint }); const tempActor = await this.#generateTempActor([document], false, false, true); const cleansedChoiceSet = deepClone(choiceSet); try { const item = tempActor.getEmbeddedDocument("Item", document._id); const choiceSetRules = new game.pf2e.RuleElements.all.ChoiceSet(cleansedChoiceSet, { parent: item }); const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat(); const choices = await choiceSetRules.inflateChoices(rollOptions, []); src_logger.debug("Starting choice evaluation", { document, choiceSet, item, choiceSetRules, rollOptions, choices, }); if (cleansedChoiceSet.choices?.query) { const nonFilteredChoices = await choiceSetRules.inflateChoices(rollOptions, [item]); const queryResults = await choiceSetRules.queryCompendium(cleansedChoiceSet.choices, rollOptions, [item]); src_logger.debug("Query Result", { queryResults, nonFilteredChoices }); } src_logger.debug("Evaluating choiceset", cleansedChoiceSet); const choiceMatch = await this.#featureChoiceMatch(document, choices, true, cleansedChoiceSet.adjustName, choiceHint); src_logger.debug("choiceMatch result", choiceMatch); if (choiceMatch) { choiceMatch.choiceQueryResults = deepClone(choices); return choiceMatch; } if (typeof cleansedChoiceSet.choices === "string" || Array.isArray(choices)) { const featureMatch = await this.#featureChoiceMatchNoUUID(document, choices, cleansedChoiceSet); if (featureMatch) { return featureMatch; } } let tempSet = deepClone(choiceSet); src_logger.debug(`Starting dynamic selection for ${document.name}`, { document, choiceSet, tempSet, Pathmuncher: this }); await choiceSetRules.preCreate({ itemSource: item, ruleSource: tempSet, pendingItems: [item], tempItems: [] }); // console.warn("chociesetdata", { // choiceSetRules, // selection: choiceSetRules.selection, // choiceSet: deepClone(choiceSet), // tempSet: deepClone(tempSet), // }); if (tempSet.selection) { const lookedUpChoice = choices.find((c) => c.value === tempSet.selection); src_logger.debug("lookedUpChoice", lookedUpChoice); if (lookedUpChoice) lookedUpChoice.choiceQueryResults = deepClone(choices); // set some common lookups here, e.g. deities are often not set! if (lookedUpChoice && cleansedChoiceSet.flag === "deity") { if (lookedUpChoice.label && lookedUpChoice.label !== "") { setProperty(this.result.character, "system.details.deity.value", lookedUpChoice.label); await this.#processGenericCompendiumLookup("deities", lookedUpChoice.label, "deity"); const camelCase = Seasoning.slugD(this.result.deity[0].system.slug); setProperty(document, `flags.pf2e.itemGrants.${camelCase}`, { id: this.result.deity[0]._id, onDelete: "detach", }); setProperty(this.result.deity[0], "flags.pf2e.grantedBy", { id: document._id, onDelete: "cascade" }); this.autoAddedFeatureIds.add(`${lookedUpChoice.value.split(".").pop()}deity`); } } return lookedUpChoice; } } catch (err) { src_logger.error("Whoa! Something went major bad wrong during choice evaluation", { err, tempActor: tempActor.toObject(), document: duplicate(document), choiceSet: duplicate(cleansedChoiceSet), }); throw err; } finally { await Actor.deleteDocuments([tempActor._id]); } src_logger.debug("Evaluate Choices failed", { choiceSet: cleansedChoiceSet, tempActor, document }); return undefined; } async #resolveInjectedUuid(document, ruleEntry) { const tempActor = await this.#generateTempActor([document], false, false); const cleansedRuleEntry = deepClone(ruleEntry); try { const item = tempActor.getEmbeddedDocument("Item", document._id); // console.warn("creating grant item"); const grantItemRule = new game.pf2e.RuleElements.all.GrantItem(cleansedRuleEntry, { parent: item }); // console.warn("Begining uuid resovle"); const uuid = grantItemRule.resolveInjectedProperties(grantItemRule.uuid, { warn: false }); src_logger.debug("uuid selection", { document, choiceSet: ruleEntry, item, grantItemRule, uuid, }); if (uuid) return uuid; } catch (err) { src_logger.error("Whoa! Something went major bad wrong during uuid evaluation", { err, tempActor: tempActor.toObject(), document: duplicate(document), ruleEntry: duplicate(cleansedRuleEntry), }); throw err; } finally { await Actor.deleteDocuments([tempActor._id]); } src_logger.debug("Evaluate UUID failed", { choiceSet: cleansedRuleEntry, tempActor, document }); return undefined; } async #checkRule(document, rule) { const tempActor = await this.#generateTempActor([document], true); const cleansedRule = deepClone(rule); try { const item = tempActor.getEmbeddedDocument("Item", document._id); const ruleElement = cleansedRule.key === "ChoiceSet" ? new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, { parent: item }) : new game.pf2e.RuleElements.all.GrantItem(cleansedRule, { parent: item }); const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat(); if (rule.predicate) { const predicate = ruleElement.resolveInjectedProperties(ruleElement.predicate); if (!predicate.test(rollOptions)) return false; } const choices = cleansedRule.key === "ChoiceSet" ? await ruleElement.inflateChoices(rollOptions, [item]) : [ruleElement.resolveValue()]; const isGood = cleansedRule.key === "ChoiceSet" ? (await this.#featureChoiceMatch(document, choices, false)) !== undefined : ruleElement.test(rollOptions); src_logger.debug("Checking rule", { tempActor, cleansedRule, item, ruleElement, rollOptions, choices, isGood, }); return isGood; } catch (err) { src_logger.error("Something has gone most wrong during rule checking", { document, rule: cleansedRule, tempActor, }); throw err; } finally { await Actor.deleteDocuments([tempActor._id]); } } async #checkRulePredicate(document, rule) { const tempActor = await this.#generateTempActor([document], true); const cleansedRule = deepClone(rule); try { const item = tempActor.getEmbeddedDocument("Item", document._id); const ruleElement = cleansedRule.key === "ChoiceSet" ? new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, { parent: item }) : new game.pf2e.RuleElements.all.GrantItem(cleansedRule, { parent: item }); const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat(); if (rule.predicate) { const predicate = ruleElement.resolveInjectedProperties(ruleElement.predicate); return predicate.test(rollOptions); } else { return true; } } catch (err) { src_logger.error("Something has gone most wrong during rule predicate checking", { document, rule: cleansedRule, tempActor, }); throw err; } finally { await Actor.deleteDocuments([tempActor._id]); } } static adjustDocumentName(featureName, label) { const localLabel = game.i18n.localize(label); if (featureName.trim().toLowerCase() === localLabel.trim().toLowerCase()) return featureName; const name = `${featureName} (${localLabel})`; const pattern = (() => { const escaped = RegExp.escape(localLabel); return new RegExp(`\\(${escaped}\\) \\(${escaped}\\)$`); })(); return name.replace(pattern, `(${localLabel})`); } // eslint-disable-next-line complexity, no-unused-vars async #addGrantedRules(document, originType = null, choiceHint = null) { if (document.system.rules.length === 0) return; src_logger.debug(`addGrantedRules for ${document.name}`, duplicate(document)); if ( hasProperty(document, "system.level.value") && document.system.level.value > this.result.character.system.details.level.value ) { return; } const rulesToKeep = []; this.allFeatureRules[document._id] = deepClone(document.system.rules); this.autoAddedFeatureRules[document._id] = []; this.promptRules[document._id] = []; let featureRenamed = false; for (const ruleEntry of document.system.rules) { src_logger.debug(`Ping ${document.name} rule key: ${ruleEntry.key}`, ruleEntry); if (!["ChoiceSet", "GrantItem"].includes(ruleEntry.key)) { // size work around due to Pathbuilder not always adding the right size to json if (ruleEntry.key === "CreatureSize") this.size = ruleEntry.value; this.autoAddedFeatureRules[document._id].push(ruleEntry); rulesToKeep.push(ruleEntry); continue; } if (NO_AUTO_CHOICE().includes(document.name)) { src_logger.debug(`Deliberately skipping ${document.name} auto choice detection`); rulesToKeep.push(ruleEntry); continue; } src_logger.debug(`Checking ${document.name} rule key: ${ruleEntry.key}`, { ruleEntry, docRules: deepClone(document.system.rules), document: deepClone(document), }); if (ruleEntry.key === "ChoiceSet" && ruleEntry.predicate) { src_logger.debug(`Checking for predicates`, { ruleEntry, document, }); const testResult = await this.#checkRulePredicate(duplicate(document), ruleEntry); if (!testResult) { const data = { document, ruleEntry, testResult }; src_logger.debug( `The test failed for ${document.name} rule key: ${ruleEntry.key} (This is probably not a problem).`, data ); rulesToKeep.push(ruleEntry); continue; } } const choice = ruleEntry.key === "ChoiceSet" ? await this.#evaluateChoices(document, ruleEntry, choiceHint) : undefined; const uuid = ruleEntry.key === "GrantItem" ? await this.#resolveInjectedUuid(document, ruleEntry) : choice?.value; if (choice?.choiceQueryResults) { ruleEntry.choiceQueryResults = choice.choiceQueryResults; } const flagName = Pathmuncher.getFlag(document, ruleEntry); // if (flagName && choice?.value && !hasProperty(document, `flags.pf2e.rulesSelections.${flagName}`)) { // setProperty(document, `flags.pf2e.rulesSelections.${flagName}`, choice.value); // } src_logger.debug(`UUID for ${document.name}: "${uuid}"`, { document, ruleEntry, choice, uuid }); const ruleFeature = uuid && typeof uuid === "string" ? await fromUuid(uuid) : undefined; // console.warn("ruleFeature", ruleFeature); if (ruleFeature) { const featureDoc = ruleFeature.toObject(); featureDoc._id = foundry.utils.randomID(); if (featureDoc.system.rules) this.allFeatureRules[featureDoc._id] = deepClone(featureDoc.system.rules); setProperty(featureDoc, "flags.pathmuncher.origin.uuid", uuid); src_logger.debug(`Found rule feature ${featureDoc.name} for ${document.name} for`, ruleEntry); if (choice) { ruleEntry.selection = choice.value; setProperty(document, `flags.pf2e.rulesSelections.${flagName}`, choice.value); } if (src_utils.isString(ruleEntry.rollOption)) { ruleEntry.rollOption = `${ruleEntry.rollOption}:${flagName}`; } if (ruleEntry.predicate && ruleEntry.key === "GrantItem") { src_logger.debug(`Checking for grantitem predicates`, { ruleEntry, document, featureDoc, }); const testResult = await this.#checkRule(featureDoc, ruleEntry); if (!testResult) { const data = { document, ruleEntry, featureDoc, testResult }; src_logger.debug( `The test failed for ${document.name} rule key: ${ruleEntry.key} (This is probably not a problem).`, data ); rulesToKeep.push(ruleEntry); // this.autoAddedFeatureRules[document._id].push(ruleEntry); continue; } else { src_logger.debug(`The test passed for ${document.name} rule key: ${ruleEntry.key}`, ruleEntry); // this.autoAddedFeatureRules[document._id].push(ruleEntry); // eslint-disable-next-line max-depth // if (!ruleEntry.flag) ruleEntry.flag = Seasoning.slugD(document.name); ruleEntry.pathmuncherImport = true; rulesToKeep.push(ruleEntry); } } // setProperty(ruleEntry, `preselectChoices.${ruleEntry.flag}`, ruleEntry.selection ?? ruleEntry.uuid); if (this.autoAddedFeatureIds.has(`${ruleFeature.id}${ruleFeature.type}`)) { src_logger.debug(`Feature ${featureDoc.name} found for ${document.name}, but has already been added (${ruleFeature.id})`, ruleFeature); // this.autoAddedFeatureRules[document._id].push(ruleEntry); // rulesToKeep.push(ruleEntry); if (ruleEntry.key === "GrantItem" && ruleEntry.flag) { this.autoAddedFeatureRules[document._id].push(ruleEntry); rulesToKeep.push(ruleEntry); } continue; } else { src_logger.debug(`Feature ${featureDoc.name} not found for ${document.name}, adding (${ruleFeature.id})`, ruleFeature); if (ruleEntry.selection || ruleEntry.flag) { rulesToKeep.push(ruleEntry); } this.autoAddedFeatureIds.add(`${ruleFeature.id}${ruleFeature.type}`); featureDoc._id = foundry.utils.randomID(); this.#createGrantedItem(featureDoc, document, { itemGrantName: flagName, applyFeatLocation: false }); if (hasProperty(featureDoc, "system.rules")) await this.#addGrantedRules(featureDoc); } } else if (getProperty(choice, "nouuid")) { src_logger.debug("Parsed no id rule", { choice, uuid, ruleEntry }); if (!ruleEntry.flag) ruleEntry.flag = Seasoning.slugD(document.name); ruleEntry.selection = choice.value; if (choice.label) document.name = `${document.name} (${choice.label})`; rulesToKeep.push(ruleEntry); } else if (choice && uuid && !hasProperty(ruleEntry, "selection")) { src_logger.debug("Parsed odd choice rule", { choice, uuid, ruleEntry }); // if (!ruleEntry.flag) ruleEntry.flag = Seasoning.slugD(document.name); ruleEntry.selection = choice.value; if ( ((!ruleEntry.adjustName && choice.label && typeof uuid === "object") || (!choice.adjustName && choice.label)) && !featureRenamed ) { document.name = Pathmuncher.adjustDocumentName(document.name, choice.label); featureRenamed = true; } rulesToKeep.push(ruleEntry); } else { src_logger.debug(`Final rule fallback for ${document.name}`, ruleEntry); const data = { uuid: ruleEntry.uuid, document, ruleEntry, choice, }; if ( ruleEntry.key === "GrantItem" && (ruleEntry.flag || ruleEntry.selection || ruleEntry.uuid.startsWith("Compendium")) ) { rulesToKeep.push(ruleEntry); } else if (ruleEntry.key === "ChoiceSet" && !hasProperty(ruleEntry, "flag")) { src_logger.debug("Prompting user for choices", ruleEntry); this.promptRules[document._id].push(ruleEntry); rulesToKeep.push(ruleEntry); } else if (ruleEntry.key === "ChoiceSet" && !choice && !uuid) { src_logger.warn("Unable to determine choice asking", data); rulesToKeep.push(ruleEntry); this.promptRules[document._id].push(ruleEntry); } src_logger.warn("Unable to determine granted rule feature, needs better parser", data); } if (ruleEntry.adjustName && choice?.label && !featureRenamed) { document.name = Pathmuncher.adjustDocumentName(document.name, choice.label); } this.autoAddedFeatureRules[document._id].push(ruleEntry); src_logger.debug(`End result for ${document.name} for a ${ruleEntry.key}`, { document: deepClone(document), rulesToKeep: deepClone(rulesToKeep), ruleEntry: deepClone(ruleEntry), choice: deepClone(choice), uuid: deepClone(uuid), }); } // eslint-disable-next-line require-atomic-updates document.system.rules = rulesToKeep; src_logger.debug(`Final status for ${document.name}`, { document: deepClone(document), rulesToKeep: deepClone(rulesToKeep), }); } async #addGrantedItems(document, { originType = null, applyFeatLocation = false, choiceHint = null } = {}) { const immediateDiveAdd = src_utils.setting("USE_IMMEDIATE_DEEP_DIVE"); const subRuleDocuments = []; if (hasProperty(document, "system.items")) { src_logger.debug(`addGrantedItems for ${document.name}`, duplicate(document)); if (!this.autoAddedFeatureItems[document._id]) { this.autoAddedFeatureItems[document._id] = duplicate(document.system.items); } const failedFeatureItems = {}; for (const [key, grantedItemFeature] of Object.entries(document.system.items).sort(([, a], [, b]) => a.level - b.level)) { src_logger.debug(`Checking ${document.name} granted item ${grantedItemFeature.name}, level(${grantedItemFeature.level}) with key: ${key}`, grantedItemFeature); if (grantedItemFeature.level > getProperty(this.result.character, "system.details.level.value")) continue; const feature = await fromUuid(grantedItemFeature.uuid); if (!feature) { const data = { uuid: grantedItemFeature.uuid, grantedFeature: grantedItemFeature, feature }; src_logger.warn("Unable to determine granted item feature, needs better parser", data); failedFeatureItems[key] = grantedItemFeature; continue; } this.autoAddedFeatureIds.add(`${feature.id}${feature.type}`); const featureDoc = feature.toObject(); featureDoc._id = foundry.utils.randomID(); setProperty(featureDoc.system, "location", document._id); this.#createGrantedItem(featureDoc, document, { originType, applyFeatLocation }); if (hasProperty(featureDoc, "system.rules")) { src_logger.debug(`Processing granted rules for granted item document ${featureDoc.name}`, duplicate(featureDoc)); if (immediateDiveAdd) { await this.#addGrantedItems(featureDoc, { originType, applyFeatLocation }); } else { subRuleDocuments.push(featureDoc); } } } // eslint-disable-next-line require-atomic-updates document.system.items = failedFeatureItems; if (!immediateDiveAdd) { for (const subRuleDocument of subRuleDocuments) { src_logger.debug( `Processing granted rules for granted item document ${subRuleDocument.name}`, duplicate(subRuleDocument) ); await this.#addGrantedItems(subRuleDocument, { originType, applyFeatLocation, choiceHint }); } } } if (hasProperty(document, "system.rules")) { src_logger.debug(`Processing granted rules for core document ${document.name}`, duplicate(document)); await this.#addGrantedRules(document, originType, choiceHint); } } #determineAbilityBoosts() { const breakdown = getProperty(this.source, "abilities.breakdown"); const useCustomStats = breakdown && breakdown.ancestryFree.length === 0 && breakdown.ancestryBoosts.length === 0 && breakdown.ancestryFlaws.length === 0 && breakdown.backgroundBoosts.length === 0 && breakdown.classBoosts.length === 0; if (breakdown && !useCustomStats) { this.boosts.custom = false; const classBoostMap = {}; for (const [key, boosts] of Object.entries(this.source.abilities.breakdown.mapLevelledBoosts)) { if (key <= this.source.level) { classBoostMap[key] = boosts.map((ability) => ability.toLowerCase()); } } setProperty(this.result.character, "system.build.attributes.boosts", classBoostMap); this.boosts.class = classBoostMap; // ancestry } else { this.boosts.custom = true; ["str", "dex", "con", "int", "wis", "cha"].forEach((key) => { const mod = Math.min(Math.max(Math.trunc((this.source.abilities[key] - 10) / 2), -5), 10) || 0; setProperty(this.result.character, `system.abilities.${key}.mod`, mod); }); } if (breakdown?.classBoosts.length > 0) { this.keyAbility = breakdown.classBoosts[0].toLowerCase(); } else { this.keyAbility = this.source.keyability; } setProperty(this.result.character, "system.details.keyability.value", this.keyAbility); } #generateBackgroundAbilityBoosts() { if (!this.result.background[0]) return; const breakdown = getProperty(this.source, "abilities.breakdown"); for (const boost of breakdown.backgroundBoosts) { for (const [key, boostSet] of Object.entries(this.result.background[0].system.boosts)) { if (this.result.background[0].system.boosts[key].selected) continue; if (boostSet.value.includes(boost.toLowerCase())) { this.result.background[0].system.boosts[key].selected = boost.toLowerCase(); break; } } } } #generateAncestryAbilityBoosts() { if (!this.result.ancestry[0]) return; const breakdown = getProperty(this.source, "abilities.breakdown"); const boosts = []; breakdown.ancestryBoosts.concat(breakdown.ancestryFree).forEach((boost) => { for (const [key, boostSet] of Object.entries(this.result.ancestry[0].system.boosts)) { if (this.result.ancestry[0].system.boosts[key].selected) continue; if (boostSet.value.includes(boost.toLowerCase())) { this.result.ancestry[0].system.boosts[key].selected = boost.toLowerCase(); boosts.push(boost.toLowerCase()); break; } } }); if (breakdown.ancestryBoosts.length === 0) { setProperty(this.result.ancestry[0], "system.alternateAncestryBoosts", boosts); } } #setAbilityBoosts() { if (this.boosts.custom) return; this.#generateBackgroundAbilityBoosts(); this.#generateAncestryAbilityBoosts(); this.result.class[0].system.boosts = this.boosts.class; } static SKILL_LOOKUP = { "acrobatics": "acr", "arcana": "arc", "athletics": "ath", "crafting": "cra", "deception": "dec", "diplomacy": "dip", "intimidation": "itm", "medicine": "med", "nature": "nat", "occultism": "occ", "performance": "prf", "religion": "rel", "society": "soc", "stealth": "ste", "survival": "sur", "thievery": "thi", }; #setSkills(removeSpecials = false) { for (const [key, value] of Object.entries(Pathmuncher.SKILL_LOOKUP)) { const calculatedValue = removeSpecials && (this.source.specials.some((s) => s.toLowerCase() === key) || this.parsed.specials.some((s) => s.name.toLowerCase() === key)) ? 0 : this.source.proficiencies[key] / 2; setProperty(this.result.character, `system.skills.${value}.rank`, calculatedValue); }; } #setSaves() { ["fortitude", "reflex", "will"].forEach((key) => { setProperty(this.result.character, `system.savingThrows.${key}`, this.source.proficiencies[key] / 2); }); } #setMartials() { ["advanced", "heavy", "light", "medium", "unarmored", "martial", "simple", "unarmed"].forEach((key) => { setProperty(this.result.character, `system.martial.${key}.rank`, this.source.proficiencies[key] / 2); }); } async #processCore() { setProperty(this.result.character, "name", this.source.name); setProperty(this.result.character, "prototypeToken.name", this.source.name); setProperty(this.result.character, "system.details.level.value", this.source.level); if (this.source.age !== "Not set") setProperty(this.result.character, "system.details.age.value", this.source.age); if (this.source.gender !== "Not set") setProperty(this.result.character, "system.details.gender.value", this.source.gender); // setProperty(this.result.character, "system.details.alignment.value", this.source.alignment); if (this.source.deity !== "Not set") setProperty(this.result.character, "system.details.deity.value", this.source.deity); this.size = Seasoning.getSizeValue(this.source.size); setProperty(this.result.character, "system.traits.size.value", this.size); setProperty(this.result.character, "system.traits.languages.value", this.source.languages.map((l) => l.toLowerCase())); this.#processSenses(); this.#determineAbilityBoosts(); this.#setSaves(); this.#setMartials(); // setProperty(this.result.character, "system.attributes.perception.rank", this.source.proficiencies.perception / 2); // setProperty(this.result.character, "system.attributes.classDC.rank", this.source.proficiencies.classDC / 2); } #indexFind(index, arrayOfNameMatches) { for (const name of arrayOfNameMatches) { const indexMatch = index.find((i) => { const slug = i.system.slug ?? Seasoning.slug(i.name); return ( slug === Seasoning.slug(name) || slug === Seasoning.slug(Seasoning.getClassAdjustedSpecialNameLowerCase(name, this.source.class)) || slug === Seasoning.slug(Seasoning.getAncestryAdjustedSpecialNameLowerCase(name, this.source.ancestry)) || slug === Seasoning.slug(Seasoning.getHeritageAdjustedSpecialNameLowerCase(name, this.source.heritage)) || (src_utils.allowDualClasses() && slug === Seasoning.slug(Seasoning.getDualClassAdjustedSpecialNameLowerCase(name, this.source.dualClass))) ); }); if (indexMatch) return indexMatch; } return undefined; } #findInPackIndexes(type, arrayOfNameMatches) { const matcher = this.compendiumMatchers[type]; for (const [packName, index] of Object.entries(matcher.indexes)) { const indexMatch = this.#indexFind(index, arrayOfNameMatches); if (indexMatch) return { i: indexMatch, pack: matcher.packs[packName] }; } return undefined; } #sortParsedFeats() { // eslint-disable-next-line complexity this.parsed.feats.sort((f1, f2) => { const f1RefUndefined = !(typeof f1.type === "string" || f1.type instanceof String); const f2RefUndefined = !(typeof f2.type === "string" || f2.type instanceof String); if (f1RefUndefined || f2RefUndefined) { if (f1RefUndefined && f2RefUndefined) { return 0; } else if (f1RefUndefined) { return 1; } else { return -1; } } else if (f1.type === "Awarded Feat" && f2.type === "Awarded Feat") { return (f1.level ?? 20) - (f2.level ?? 20); } else if (f1.type === "Awarded Feat") { return 1; } else if (f2.type === "Awarded Feat") { return -1; } else if ((f1.level ?? 20) === (f2.level ?? 20)) { const f1Index = constants.FEAT_PRIORITY.indexOf(f1.type); const f2Index = constants.FEAT_PRIORITY.indexOf(f2.type); if (f1Index > f2Index) { return 1; } else if (f1Index < f2Index) { return -1; } else { return 0; } } else { return (f1.level ?? 20) - (f2.level ?? 20); } }); } // eslint-disable-next-line complexity async #generateFeatItems(type, { levelCap = null, typeFilter = null, excludeChild = false, excludeParents = false, excludeStandard = false } = {} ) { src_logger.debug(`Generate feat items for ${type} with level cap "${levelCap}" and filter "${typeFilter}"`); for (const featArray of [this.parsed.feats, this.parsed.specials]) { for (const pBFeat of featArray) { if (pBFeat.added) continue; if (levelCap && (pBFeat.level ?? 20) > levelCap) continue; if (typeFilter && pBFeat.type !== typeFilter) continue; if (excludeChild && pBFeat.isChild === true) continue; if (excludeParents && pBFeat.isParent === true) continue; if (excludeStandard && pBFeat.isStandard === true) continue; src_logger.debug(`Generating feature for ${pBFeat.name}`, pBFeat); if (this.devMode) src_logger.error(`Generating feature for ${pBFeat.name}`, { pBFeat, this: this }); const indexMatch = this.#findInPackIndexes(type, [pBFeat.name, pBFeat.originalName]); const displayName = pBFeat.extra ? Pathmuncher.adjustDocumentName(pBFeat.name, pBFeat.extra) : pBFeat.name; if (!indexMatch) { src_logger.debug(`Unable to match feat ${displayName}`, { displayName, name: pBFeat.name, extra: pBFeat.extra, pBFeat, type, }); this.check[pBFeat.originalName] = { name: displayName, type: "feat", details: { displayName, name: pBFeat.name, originalName: pBFeat.originalName, extra: pBFeat.extra, pBFeat, type, }, }; continue; } if (this.check[pBFeat.originalName]) delete this.check[pBFeat.originalName]; pBFeat.added = true; if (this.autoAddedFeatureIds.has(`${indexMatch._id}${indexMatch.type}`)) { src_logger.debug("Feat included in class features auto add", { displayName, pBFeat, type }); pBFeat.addedAutoId = `${indexMatch._id}_${indexMatch.type}`; continue; } const doc = await indexMatch.pack.getDocument(indexMatch.i._id); const docData = doc.toObject(); docData._id = foundry.utils.randomID(); pBFeat.addedId = docData._id; // docData.name = displayName; this.#generateFoundryFeatLocation(docData, pBFeat); this.result.feats.push(docData); const options = { originType: typeFilter, applyFeatLocation: false, choiceHint: pBFeat.extra && pBFeat.extra !== "" ? pBFeat.extra : null, }; await this.#addGrantedItems(docData, "feat", options); } } } async #generateSpecialItems(type) { for (const special of this.parsed.specials) { if (special.added) continue; src_logger.debug("Generating special for", special); const indexMatch = this.#findInPackIndexes(type, [special.name, special.originalName]); if (!indexMatch) { src_logger.debug(`Unable to match special ${special.name}`, { special: special.name, type }); this.check[special.originalName] = { name: special.name, type: "special", details: { displayName: special.name, name: special.name, originalName: special.originalName, special }, }; continue; } special.added = true; if (this.check[special.originalName]) delete this.check[special.originalName]; if (this.autoAddedFeatureIds.has(`${indexMatch._id}${indexMatch.type}`)) { src_logger.debug("Special included in class features auto add", { special: special.name, type }); special.addedAutoId = `${indexMatch._id}_${indexMatch.type}`; continue; } const doc = await indexMatch.pack.getDocument(indexMatch.i._id); const docData = doc.toObject(); docData._id = foundry.utils.randomID(); special.addedId = docData._id; this.result.feats.push(docData); await this.#addGrantedItems(docData, { applyFeatLocation: true }); } } #resizeItem(item) { if (Seasoning.isPhysicalItemType(item.type)) { const resizeItem = item.type !== "treasure" && !["med", "sm"].includes(this.size); if (resizeItem) item.system.size = this.size; } } async #generateAdventurersPack() { const defaultCompendium = game.packs.get("pf2e.equipment-srd"); const index = await defaultCompendium.getIndex({ fields: ["name", "type", "system.slug"] }); const adventurersPack = this.parsed.equipment.find((e) => e.pbName === "Adventurer's Pack"); if (adventurersPack) { const compendiumBackpack = await defaultCompendium.getDocument("3lgwjrFEsQVKzhh7"); const backpackInstance = compendiumBackpack.toObject(); adventurersPack.added = true; backpackInstance._id = foundry.utils.randomID(); adventurersPack.addedId = backpackInstance._id; this.result.adventurersPack.item = adventurersPack; this.result.equipment.push(backpackInstance); for (const content of this.result.adventurersPack.contents) { const indexMatch = index.find((i) => i.system.slug === content.slug); if (!indexMatch) { src_logger.error(`Unable to match adventurers kit item ${content.name}`, content); continue; } const doc = await defaultCompendium.getDocument(indexMatch._id); const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); itemData.system.quantity = content.qty; itemData.system.containerId = backpackInstance?._id; this.#resizeItem(itemData); this.result.equipment.push(itemData); } } } async #generateContainers() { for (const [key, data] of Object.entries(this.source.equipmentContainers)) { if (data.foundryId) continue; const name = Seasoning.getFoundryEquipmentName(data.containerName); const indexMatch = this.compendiumMatchers["equipment"].getMatch(data.containerName, name); const id = foundry.utils.randomID(); const doc = indexMatch ? await indexMatch.pack.getDocument(indexMatch.i._id) : await Item.create({ name: data.containerName, type: "backpack" }, { temporary: true }); const itemData = doc.toObject(); itemData._id = id; this.#resizeItem(itemData); this.result["equipment"].push(itemData); this.parsed.equipment.push({ foundryName: name, pbName: data.containerName, originalName: data.containerName, name, qty: 1, added: true, inContainer: undefined, container: this.#getContainerData(key), foundryId: id, }); } } async #generateEquipmentItems() { for (const e of this.parsed.equipment) { if (e.pbName === "Adventurer's Pack") continue; if (e.added) continue; if (Seasoning.IGNORED_EQUIPMENT().includes(e.pbName)) { e.added = true; e.addedAutoId = "ignored"; continue; } src_logger.debug("Generating item for", e); const indexMatch = this.compendiumMatchers["equipment"].getMatch(e.pbName, e.foundryName); if (!indexMatch) { src_logger.error(`Unable to match ${e.pbName}`, e); this.bad.push({ pbName: e.pbName, type: "equipment", details: { e } }); continue; } const doc = await indexMatch.pack.getDocument(indexMatch.i._id); if (doc.type != "kit") { const itemData = doc.toObject(); itemData._id = e.foundryId || foundry.utils.randomID(); itemData.system.quantity = e.qty; const type = doc.type === "treasure" ? "treasure" : "equipment"; if (e.inContainer) { const containerMatch = this.parsed.equipment.find((con) => con.container?.id === e.inContainer); if (containerMatch) { itemData.system.containerId = containerMatch.foundryId; itemData.system.equipped.carryType = "stowed"; } } if (e.invested) { itemData.system.equipped.carryType = "worn"; itemData.system.equipped.invested = true; itemData.system.equipped.inSlot = true; itemData.system.equipped.handsHeld = 0; } this.#resizeItem(itemData); this.result[type].push(itemData); e.addedId = itemData._id; } // eslint-disable-next-line require-atomic-updates e.added = true; } } async #processEquipmentItems() { // just in case it's in the equipment, pathbuilder should have translated this to items await this.#generateAdventurersPack(); await this.#generateContainers(); await this.#generateEquipmentItems(); } static RUNE_SCALE = [ "", "Minor", "Lesser", "Moderate", "Greater", "Major", "Supreme", ]; static REINFORCING_DATA = { "Minor": { value: 1, hp: 44, }, "Lesser": { value: 2, hp: 52, }, "Moderate": { value: 3, hp: 64, }, "Greater": { value: 4, hp: 80, }, "Major": { value: 5, hp: 84, }, "Supreme": { value: 6, hp: 108, }, }; static POTENCY_SCALE = [ "", "striking", "greaterStriking", "majorStriking", ]; static RESILIENT_SCALE = [ "", "resilient", "greaterResilient", "majorResilient", ]; // eslint-disable-next-line complexity static applyRunes(parsedItem, itemData, type) { if (itemData.type == "shield") { parsedItem.runes.forEach((rune) => { if (rune.startsWith("Reinforcing")) { const runeScale = rune.split("(").pop().split(")").shift().trim(); const runeMatch = Pathmuncher.REINFORCING_DATA[runeScale]; if (runeMatch) { itemData.system.runes.reinforcing = runeMatch.value; itemData.system.hp.value += runeMatch.hp; } } else { const runeScale = rune.split("(").pop().split(")").shift().trim(); const runeLevel = Pathmuncher.RUNE_SCALE.indexOf(runeScale); const runeType = rune.split("(").shift().toLowerCase().trim(); if (runeLevel !== -1) { itemData.system.runes[runeType] = runeLevel; } } }); } else if (hasProperty(itemData, "system.runes.potency")) { itemData.system.runes.potency = parsedItem.pot; if (type === "weapon") { const striking = Pathmuncher.POTENCY_SCALE.indexOf(parsedItem.str); if (striking !== -1) itemData.system.runes.striking = striking; } else if (type === "armor") { const resilient = Pathmuncher.RESILIENT_SCALE.indexOf(parsedItem.res); if (resilient !== -1) itemData.system.runes.resilient = resilient; } } if (type === "armor" && parsedItem.worn && ((Number.isInteger(parsedItem.pot) && parsedItem.pot > 0) || (parsedItem.res && parsedItem.res !== "") ) ) { itemData.system.equipped.invested = true; } if (hasProperty(itemData, "system.runes.property")) { parsedItem.runes.forEach((property) => { const resistantRegex = /Energy Resistant - (.*)/i; const resistantMatch = property.match(resistantRegex); const rune = resistantMatch ? `${resistantMatch[1]} Resistant` : property; itemData.system.runes.property.push(Seasoning.slugD(rune)); }); } if (parsedItem.mat) { const material = parsedItem.mat.split(" (")[0]; itemData.system.material.type = Seasoning.slugD(material); itemData.system.material.grade = Seasoning.getMaterialGrade(parsedItem.mat); } } async #createWeaponItem(data) { // { pbName, name, prof, qty, die, display, increasedDice, pot, str, mat, runes, attack, damageBonus, extraDamage, damageType } src_logger.debug("Generating weapon for", data); const indexMatch = this.compendiumMatchers["equipment"].getMatch(data.pbName, data.foundryName); if (!indexMatch) { src_logger.error(`Unable to match weapon item ${data.name}`, data); this.bad.push({ pbName: data.pbName, type: "weapon", details: { w: data } }); return null; } const doc = await indexMatch.pack.getDocument(indexMatch.i._id); const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); itemData.system.quantity = data.qty; // because some shields don't have damage dice, but come in as weapons on pathbuilder if (itemData.type === "weapon") { if (data.die) itemData.system.damage.die = data.die; Pathmuncher.applyRunes(data, itemData, "weapon"); } if (data.display.startsWith("Large ") || data.increasedDice) { itemData.system.size = "lg"; } else if (data.display && !Seasoning.IGNORED_EQUIPMENT_DISPLAY(data.display)) { itemData.name = data.display; } this.#resizeItem(itemData); this.result.weapons.push(itemData); data.added = true; data.addedId = itemData._id; return itemData; } async #generateWeaponItems() { for (const w of this.parsed.weapons) { if (Seasoning.IGNORED_EQUIPMENT().includes(w.pbName)) { w.added = true; w.addedAutoId = "ignored"; continue; } await this.#createWeaponItem(w); } } #adjustArmorItem(itemData, parsedArmor) { itemData._id = foundry.utils.randomID(); itemData.system.equipped.value = parsedArmor.worn ?? false; if (!Seasoning.RESTRICTED_EQUIPMENT().some((i) => itemData.name.startsWith(i))) { itemData.system.equipped.inSlot = parsedArmor.worn ?? false; itemData.system.quantity = parsedArmor.qty; const isShield = parsedArmor.prof === "shield"; itemData.system.equipped.handsHeld = isShield && parsedArmor.worn ? 1 : 0; itemData.system.equipped.carryType = isShield && parsedArmor.worn ? "held" : "worn"; Pathmuncher.applyRunes(parsedArmor, itemData, "armor"); } if (parsedArmor.display) itemData.name = parsedArmor.display; this.#resizeItem(itemData); return itemData; } async #generateArmorItems() { for (const a of this.parsed.armor) { if (Seasoning.IGNORED_EQUIPMENT().includes(a.pbName)) { a.added = true; a.addedAutoId = "ignored"; continue; } src_logger.debug("Generating armor for", a); if (Seasoning.GRANTED_ITEMS_LIST().includes(a.pbName)) { const existingItem = this.result.armor.find((i) => i.name === a.foundryName); if (existingItem) { a.added = true; a.addedId = existingItem._id; src_logger.debug(`Ignoring armor item ${a.pbName} as it has been granted by a feature`); continue; } } const indexMatch = this.compendiumMatchers["equipment"].getMatch(a.foundryName, `${a.pbName} Armor`); if (!indexMatch) { src_logger.error(`Unable to match armor kit item ${a.name}`, a); this.bad.push({ pbName: a.pbName, type: "armor", details: { a } }); continue; } const doc = await indexMatch.pack.getDocument(indexMatch.i._id); const itemData = this.#adjustArmorItem(doc.toObject(), a); this.result.armor.push(itemData); a.addedId = itemData._id; a.added = true; } } getClassSpellCastingType(dual = false) { const classCaster = dual ? this.source.spellCasters.find((caster) => caster.name === this.source.dualClass) : this.source.spellCasters.find((caster) => caster.name === this.source.class); const type = classCaster?.spellcastingType; if (type || this.source.spellCasters.length === 0) return type ?? "spontaneous"; // if no type and multiple spell casters, then return the first spell casting type return this.source.spellCasters[0].spellcastingType ?? "spontaneous"; } // aims to determine the class magic tradition for a spellcasting block getClassMagicTradition(caster) { const classCaster = [this.source.class, this.source.dualClass].includes(caster.name); const tradition = classCaster ? caster?.magicTradition : undefined; // if a caster tradition or no spellcasters, return divine if (tradition || this.source.spellCasters.length === 0) return tradition ?? "divine"; // not a focus traditions if (caster.magicTradition !== "focus" && ["divine", "occult", "primal", "arcane"].includes(caster.magicTradition)) { return caster.magicTradition; } // this spell caster type is not a class, determine class tradition based on ability const abilityTradition = this.source.spellCasters.find((c) => [this.source.class, this.source.dualClass].includes(c.name) && c.ability === caster.ability ); if (abilityTradition) return abilityTradition.magicTradition; // if no type and multiple spell casters, then return the first spell casting type return this.source.spellCasters[0].magicTradition && this.source.spellCasters[0].magicTradition !== "focus" ? this.source.spellCasters[0].magicTradition : "divine"; } #applySpellBlending(spellcastingEntity, caster) { if (caster.blendedSpells.length === 0) return; const remove = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; const add = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // find adjustments caster.blendedSpells.forEach((slot) => { remove[slot.levelFrom]++; add[slot.LevelTo]++; }); for (let i = 0; i <= 10; i++) { const toAdd = this.options.adjustBlendedSlots ? 0 : Math.floor(add[i] / 2); const toRemove = this.options.adjustBlendedSlots ? remove[i] : 0; const adjustment = 0 - toRemove - toAdd; src_logger.debug("Adjusting spells for spell blending", { i, adjustment, add, remove, toAdd, max: spellcastingEntity.slots[`slot${i}`].max }); spellcastingEntity.slots[`slot${i}`].max += adjustment; spellcastingEntity.slots[`slot${i}`].value += adjustment; } } #generateSpellCaster(caster) { const isFocus = caster.magicTradition === "focus"; const magicTradition = this.getClassMagicTradition(caster); const spellcastingType = isFocus ? "focus" : caster.spellcastingType; const flexible = false; // placeholder const name = isFocus ? `${src_utils.capitalize(magicTradition)} ${caster.name}` : caster.name; const spellcastingEntity = { ability: { value: caster.ability, }, proficiency: { value: caster.proficiency / 2, }, spelldc: { item: 0, }, tradition: { value: magicTradition, }, prepared: { value: spellcastingType, flexible, }, slots: {}, showUnpreparedSpells: { value: true }, showSlotlessLevels: { value: true }, }; // apply slot data for (let i = 0; i <= 10; i++) { spellcastingEntity.slots[`slot${i}`] = { max: caster.perDay[i], prepared: {}, value: caster.perDay[i], }; } // adjust slots for spell blended effects this.#applySpellBlending(spellcastingEntity, caster); const data = { _id: foundry.utils.randomID(), name, type: "spellcastingEntry", system: spellcastingEntity, }; this.result.casters.push(data); return data; } #generateFocusSpellCaster(proficiency, ability, tradition) { const data = { _id: foundry.utils.randomID(), name: `${src_utils.capitalize(tradition)} Focus Tradition`, type: "spellcastingEntry", system: { ability: { value: ability, }, proficiency: { value: proficiency / 2, }, spelldc: { item: 0, }, tradition: { value: tradition, }, prepared: { value: "focus", flexible: false, }, showUnpreparedSpells: { value: true }, }, }; this.result.casters.push(data); return data; } async #loadSpell(spell, casterId, debugData) { const spellName = spellRename(spell.split("(")[0].trim()); src_logger.debug("focus spell details", { spell, spellName, debugData }); const indexMatch = this.compendiumMatchers["spells"].getMatch(spell, spellName, true); if (!indexMatch) { if (debugData.psychicAmpSpell) return undefined; src_logger.error(`Unable to match focus spell ${spell}`, { spell, spellName, debugData }); this.bad.push({ pbName: spell, type: "spell", details: { originalName: spell, name: spellName, debugData } }); return undefined; } const doc = await indexMatch.pack.getDocument(indexMatch.i._id); const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); itemData.system.location.value = casterId; return itemData; } // eslint-disable-next-line complexity async #processCasterSpells(instance, caster, spellEnhancements, forcePrepare = false) { const spellNames = {}; for (const spellSelection of caster.spells) { const level = spellSelection.spellLevel; const preparedAtLevel = caster.prepared?.length > 0 ? (caster.prepared.find((p) => p.spellLevel === level)?.list ?? []) : []; let preparedValue = 0; // const preparedMap = preparedAtLevel.reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map()); for (const [i, spell] of spellSelection.list.entries()) { src_logger.debug(`Checking spell at ${i} for level ${level}`, { spell }); const itemData = await this.#loadSpell(spell, instance._id, { spellSelection, list: spellSelection.list, level, instance, }); if (itemData) { itemData.system.location.heightenedLevel = level; spellNames[spell] = itemData._id; this.result.spells.push(itemData); // if the caster is prepared we don't prepare spells as all known spells come through in JSON if (instance.system.prepared.value !== "prepared" || spellEnhancements?.preparePBSpells || forcePrepare || (caster.spellcastingType === "prepared" && preparedAtLevel.length === 0 && spellSelection.list.length <= caster.perDay[level]) ) { src_logger.debug(`Preparing spell ${itemData.name} for level ${level}`, { spell }); // eslint-disable-next-line require-atomic-updates instance.system.slots[`slot${level}`].prepared[preparedValue] = { id: itemData._id }; preparedValue++; } } } for (const spell of preparedAtLevel) { // if (spellNames.includes(spellName)) continue; const parsedSpell = getProperty(spellNames, spell); const itemData = parsedSpell ? this.result.spells.find((s) => s._id === parsedSpell) : await this.#loadSpell(spell, instance._id, { spellSelection, level, instance, }); if (itemData) { itemData.system.location.heightenedLevel = level; if (itemData && !parsedSpell) { spellNames[spell] = itemData._id; this.result.spells.push(itemData); } src_logger.debug(`Preparing spell ${itemData.name} for level ${level}`, { spellName: spell }); // eslint-disable-next-line require-atomic-updates instance.system.slots[`slot${level}`].prepared[preparedValue] = { id: itemData._id }; preparedValue++; } else { src_logger.warn(`Unable to find spell ${spell}`); } } if (spellEnhancements?.knownSpells) { for (const spell of spellEnhancements.knownSpells) { const itemData = await this.#loadSpell(spell, instance._id, { spellEnhancements, instance, }); if (itemData && !hasProperty(spellNames, itemData.name)) { itemData.system.location.heightenedLevel = level; spellNames[spell] = itemData._id; this.result.spells.push(itemData); } } } } } async #processFocusSpells(instance, spells) { for (const spell of spells) { const itemData = await this.#loadSpell(spell, instance._id, { instance, spells, spell, }); if (itemData) this.result.spells.push(itemData); if (spell.endsWith("(Amped)")) { const psychicSpell = spell.replace("(Amped)", "(Psychic)"); const psychicItemData = await this.#loadSpell(psychicSpell, instance._id, { instance, spells, spell: psychicSpell, psychicAmpSpell: true, }); if (psychicItemData) { this.result.spells.push(psychicItemData); } } } } async #processRituals() { if (!this.source.rituals) return; const ritualCompendium = new CompendiumMatcher({ type: "spells", indexFields: ["name", "type", "system.slug", "system.ritual"], }); await ritualCompendium.loadCompendiums(); for (const ritual of this.source.rituals) { const ritualName = ritual.split("(")[0].trim(); src_logger.debug("focus spell details", { ritual, spellName: ritualName }); const indexMatch = this.compendiumMatchers["spells"].getNameMatchWithFilter(ritualName, ritualName); if (!indexMatch || !hasProperty(indexMatch, "system.ritual")) { src_logger.error(`Unable to match ritual spell ${ritual}`, { spell: ritual, spellName: ritualName }); this.bad.push({ pbName: ritual, type: "spell", details: { originalName: ritual, name: ritualName } }); continue; } const doc = await indexMatch.pack.getDocument(indexMatch.i._id); const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); this.result.spells.push(itemData); } } async #processSpells() { for (const caster of this.source.spellCasters) { src_logger.debug("Generating caster for", caster); if (Number.isInteger(parseInt(caster.focusPoints))) this.result.focusPool += caster.focusPoints; const instance = this.#generateSpellCaster(caster); src_logger.debug("Generated caster instance", instance); const spellEnhancements = Seasoning.getSpellCastingFeatureAdjustment(caster.name); let forcePrepare = false; if (hasProperty(spellEnhancements, "showSlotless")) { instance.system.showSlotlessLevels.value = getProperty(spellEnhancements, "showSlotless"); } else if ( caster.spellcastingType === "prepared" && ![this.source.class, this.source.dualClass].includes(caster.name) ) { const slotToPreparedMatch = caster.spells.every((spellBlock) => { const spellCount = spellBlock.list.length; const perDay = caster.perDay[spellBlock.spellLevel]; return perDay === spellCount; }); src_logger.debug(`Setting ${caster.name} show all slots to ${!slotToPreparedMatch}`); instance.system.showSlotlessLevels.value = !slotToPreparedMatch; forcePrepare = slotToPreparedMatch; } await this.#processCasterSpells(instance, caster, spellEnhancements, forcePrepare); } for (const tradition of ["occult", "primal", "divine", "arcane"]) { const traditionData = getProperty(this.source, `focus.${tradition}`); src_logger.debug(`Checking for focus tradition ${tradition}`); if (!traditionData) continue; for (const ability of ["str", "dex", "con", "int", "wis", "cha"]) { const abilityData = getProperty(traditionData, ability); src_logger.debug(`Checking for focus tradition ${tradition} with ability ${ability}`); if (!abilityData) continue; src_logger.debug("Generating focus spellcasting ", { tradition, traditionData, ability }); const instance = this.#generateFocusSpellCaster(abilityData.proficiency, ability, tradition); if (abilityData.focusCantrips && abilityData.focusCantrips.length > 0) { await this.#processFocusSpells(instance, abilityData.focusCantrips); } if (abilityData.focusSpells && abilityData.focusSpells.length > 0) { await this.#processFocusSpells(instance, abilityData.focusSpells); } } } setProperty(this.result.character, "system.resources.focus.max", this.source.focusPoints); setProperty(this.result.character, "system.resources.focus.value", this.source.focusPoints); } async #generateLores() { for (const lore of this.source.lores) { const data = { name: lore[0], type: "lore", system: { proficient: { value: lore[1] / 2, }, featType: "", mod: { value: 0, }, item: { value: 0, }, }, }; this.result.lores.push(data); } } async #generateMoney() { const compendium = game.packs.get("pf2e.equipment-srd"); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); const moneyLookup = [ { slug: "platinum-pieces", type: "pp" }, { slug: "gold-pieces", type: "gp" }, { slug: "silver-pieces", type: "sp" }, { slug: "copper-pieces", type: "cp" }, ]; for (const lookup of moneyLookup) { const indexMatch = index.find((i) => i.system.slug === lookup.slug); if (indexMatch) { const doc = await compendium.getDocument(indexMatch._id); const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); itemData.system.quantity = this.source.money[lookup.type]; this.result.money.push(itemData); } } } async #processFormulas() { const uuids = []; for (const formulaSource of this.source.formula) { for (const formulaName of formulaSource.known) { const indexMatch = this.compendiumMatchers["formulas"].getMatch(formulaName, formulaName); if (!indexMatch) { src_logger.error(`Unable to match formula ${formulaName}`, { formulaSource, name: formulaName }); this.bad.push({ pbName: formulaName, type: "formula", details: { formulaSource, name: formulaName } }); continue; } const doc = await indexMatch.pack.getDocument(indexMatch.i._id); uuids.push({ uuid: doc.uuid }); } } setProperty(this.result.character, "system.crafting.formulas", uuids); } async #processFeats() { this.#sortParsedFeats(); // pre pass for standard items for (let i = 1; i <= this.result.character.system.details.level.value; i++) { await this.#generateFeatItems("feats", { typeFilter: "Ancestry Feat", levelCap: i, excludeChild: true, excludeParents: true }); await this.#generateFeatItems("feats", { typeFilter: "Skill Feat", levelCap: i, excludeChild: true, excludeParents: true }); await this.#generateFeatItems("feats", { typeFilter: "Class Feat", levelCap: i, excludeChild: true, excludeParents: true }); await this.#generateFeatItems("feats", { typeFilter: "General Feat", levelCap: i, excludeChild: true, excludeParents: true }); } await this.#generateFeatItems("ancestryFeatures", { excludeChild: true, excludeParents: true }); // prepass for non-child items for (let i = 1; i <= this.result.character.system.details.level.value; i++) { await this.#generateFeatItems("feats", { typeFilter: "Ancestry Feat", levelCap: i, excludeChild: true }); await this.#generateFeatItems("feats", { typeFilter: "Skill Feat", levelCap: i, excludeChild: true }); await this.#generateFeatItems("feats", { typeFilter: "Class Feat", levelCap: i, excludeChild: true }); await this.#generateFeatItems("feats", { typeFilter: "General Feat", levelCap: i, excludeChild: true }); await this.#generateFeatItems("feats", { typeFilter: "Archetype Feat", levelCap: i, excludeChild: true }); } await this.#generateFeatItems("ancestryFeatures", { excludeChild: true }); this.#setSkills(); // final pass, include all this.#statusUpdate(1, 5, "Feats"); await this.#generateFeatItems("feats"); this.#statusUpdate(2, 5, "Feats"); await this.#generateFeatItems("ancestryFeatures"); this.#statusUpdate(3, 5, "Feats"); await this.#generateSpecialItems("ancestryFeatures"); this.#statusUpdate(4, 5, "Feats"); await this.#generateSpecialItems("classFeatures"); this.#statusUpdate(5, 5, "Feats"); await this.#generateSpecialItems("actions"); } async #processEquipment() { this.#statusUpdate(1, 4, "Equipment"); await this.#processEquipmentItems(); this.#statusUpdate(2, 4, "Weapons"); await this.#generateWeaponItems(); this.#statusUpdate(3, 4, "Armor"); await this.#generateArmorItems(); this.#statusUpdate(2, 4, "Money"); await this.#generateMoney(); } async #generateTempActor(documents = [], includePassedDocumentsRules = false, includeGrants = false, includeFlagsOnly = false) { const actorData = mergeObject({ type: "character" }, this.result.character); actorData.name = `Mr Temp (${this.result.character.name})`; if (documents.map((d) => d.name.split("(")[0].trim().toLowerCase()).includes("skill training")) { delete actorData.system.skills; } const actor = await Actor.create(actorData, { renderSheet: false }); const currentState = duplicate(this.result); // console.warn("Initial temp actor", deepClone(actor)); const currentItems = [ ...currentState.deity, ...currentState.ancestry, ...currentState.heritage, ...currentState.background, ...currentState.class, ...currentState.lores, ...currentState.feats, ...currentState.casters, // ...currentState.spells, // ...currentState.equipment, // ...currentState.weapons, // ...currentState.armor, // ...currentState.treasure, // ...currentState.money, ]; for (const doc of documents) { if (!currentItems.some((d) => d._id === doc._id)) { currentItems.push(deepClone(doc)); } } try { // if the rule selected is an object, id doesn't take on import const ruleUpdates = []; for (const i of deepClone(currentItems)) { if (!i.system.rules || i.system.rules.length === 0) continue; const isPassedDocument = documents.some((d) => d._id === i._id); if (isPassedDocument && !includePassedDocumentsRules && !includeFlagsOnly) continue; const objectSelectionRules = i.system.rules .filter((r) => { const evaluateRules = ["RollOption", "ChoiceSet"].includes(r.key) && r.selection; return !includeFlagsOnly || evaluateRules; // && ["RollOption", "GrantItem", "ChoiceSet", "ActiveEffectLike"].includes(r.key); // || (["ChoiceSet"].includes(r.key) && r.selection); }) .map((r) => { r.ignored = false; return r; }); if (objectSelectionRules.length > 0) { ruleUpdates.push({ _id: i._id, system: { rules: objectSelectionRules, }, }); } } // console.warn("Rule updates", duplicate(ruleUpdates)); const items = duplicate(currentItems).map((i) => { if (i.system.items) i.system.items = []; if (i.system.rules) { i.system.rules = i.system.rules .filter((r) => { const isPassedDocument = documents.some((d) => d._id === i._id); const isChoiceSetSelection = ["ChoiceSet"].includes(r.key) && r.selection; // const choiceSetSelectionObject = isChoiceSetSelection && utils.isObject(r.selection); const choiceSetSelectionNotObject = isChoiceSetSelection && !src_utils.isObject(r.selection); // const grantRuleWithFlag = includeGrants && ["GrantItem"].includes(r.key) && r.flag; const grantRuleWithoutFlag = includeGrants && ["GrantItem"].includes(r.key) && !r.flag; // const genericDiscardRule = ["ChoiceSet", "GrantItem", "ActiveEffectLike", "Resistance", "Strike", "AdjustModifier"].includes(r.key); const genericDiscardRule = ["ChoiceSet", "GrantItem"].includes(r.key); const grantRuleFromItemFlag = includeGrants && ["GrantItem"].includes(r.key) && r.uuid.startsWith("{item|flags"); const rollOptionsRule = ["RollOption"].includes(r.key); const notPassedDocumentRules = !isPassedDocument && (choiceSetSelectionNotObject // || grantRuleWithFlag || grantRuleWithoutFlag || !genericDiscardRule); const passedDocumentRules = isPassedDocument && includePassedDocumentsRules && (isChoiceSetSelection || grantRuleWithoutFlag || grantRuleFromItemFlag || rollOptionsRule); return notPassedDocumentRules || passedDocumentRules; }) .map((r) => { // if choices is a string or an object then we replace with the query string results if ((src_utils.isString(r.choices) || src_utils.isObject(r.choices)) && r.choiceQueryResults) { r.choices = r.choiceQueryResults; } r.ignored = false; return r; }); } return i; }); // const items2 = duplicate(currentItems).map((i) => { // if (i.system.items) i.system.items = []; // if (i.system.rules) i.system.rules = i.system.rules.filter((r) => // (!documents.some((d) => d._id === i._id) // && ((["ChoiceSet",].includes(r.key) && r.selection) // // || (["GrantItem"].includes(r.key) && r.flag) // || !["ChoiceSet", "GrantItem"].includes(r.key) // )) // || (includePassedDocumentsRules && documents.some((d) => d._id === i._id) && ["ChoiceSet",].includes(r.key) && r.selection) // ).map((r) => { // if ((typeof r.choices === 'string' || r.choices instanceof String) // || (typeof r.choices === 'object' && !Array.isArray(r.choices) && r.choices !== null && r.choiceQueryResults) // ) { // r.choices = r.choiceQueryResults; // } // r.ignored = false; // return r; // }); // return i; // }); // console.warn("temp items", { // documents: deepClone(currentItems), // items: deepClone(items), // // items2: deepClone(items2), // // diff: diffObject(items, items2), // includePassedDocumentsRules, // includeGrants, // }); await actor.createEmbeddedDocuments("Item", items, { keepId: true }); // console.warn("restoring selection rules to temp items", ruleUpdates); await actor.updateEmbeddedDocuments("Item", ruleUpdates); const itemUpdates = []; for (const [key, value] of Object.entries(this.autoAddedFeatureItems)) { itemUpdates.push({ _id: `${key}`, system: { items: deepClone(value), }, }); } // console.warn("restoring feature items to temp items", itemUpdates); await actor.updateEmbeddedDocuments("Item", itemUpdates); src_logger.debug("Final temp actor", actor); } catch (err) { src_logger.error("Temp actor creation failed", { actor, documents, thisData: deepClone(this.result), actorData, err, currentItems, this: this, }); } return actor; } async processCharacter() { if (!this.source) return; await this.#prepare(); this.#statusUpdate(1, 12, "Character"); await this.#processCore(); this.#statusUpdate(2, 12, "Formula"); await this.#processFormulas(); this.#statusUpdate(3, 12, "Deity"); await this.#processGenericCompendiumLookup("deities", this.source.deity, "deity"); this.#statusUpdate(4, 12, "Ancestry"); await this.#processGenericCompendiumLookup("ancestries", this.source.ancestry, "ancestry"); this.#statusUpdate(5, 12, "Heritage"); await this.#processGenericCompendiumLookup("heritages", this.source.heritage, "heritage"); this.#statusUpdate(6, 12, "Background"); await this.#processGenericCompendiumLookup("backgrounds", this.source.background, "background"); this.#setSkills(true); this.#statusUpdate(7, 12, "Class"); await this.#processGenericCompendiumLookup("classes", this.source.class, "class"); this.#setAbilityBoosts(); this.#statusUpdate(8, 12, "FeatureRec"); await this.#processFeats(); this.#statusUpdate(10, 12, "Equipment"); await this.#processEquipment(); this.#statusUpdate(11, 12, "Spells"); await this.#processSpells(); this.#statusUpdate(11, 12, "Rituals"); await this.#processRituals(); this.#statusUpdate(12, 12, "Lores"); await this.#generateLores(); } async #removeDocumentsToBeUpdated() { const moneyIds = this.actor.items.filter((i) => i.type === "treasure" && ["Platinum Pieces", "Gold Pieces", "Silver Pieces", "Copper Pieces"].includes(i.name) ); const classIds = this.actor.items.filter((i) => i.type === "class").map((i) => i._id); const deityIds = this.actor.items.filter((i) => i.type === "deity").map((i) => i._id); const backgroundIds = this.actor.items.filter((i) => i.type === "background").map((i) => i._id); const heritageIds = this.actor.items.filter((i) => i.type === "heritage").map((i) => i._id); const ancestryIds = this.actor.items.filter((i) => i.type === "ancestry").map((i) => i._id); const treasureIds = this.actor.items .filter((i) => i.type === "treasure" && !moneyIds.includes(i.id)) .map((i) => i._id); const featIds = this.actor.items.filter((i) => i.type === "feat").map((i) => i._id); const actionIds = this.actor.items.filter((i) => i.type === "action").map((i) => i._id); const equipmentIds = this.actor.items .filter((i) => i.type === "equipment" || i.type === "backpack" || i.type === "consumable") .map((i) => i._id); const weaponIds = this.actor.items.filter((i) => i.type === "weapon").map((i) => i._id); const armorIds = this.actor.items.filter((i) => i.type === "armor").map((i) => i._id); const loreIds = this.actor.items.filter((i) => i.type === "lore").map((i) => i._id); const spellIds = this.actor.items .filter((i) => i.type === "spell" || i.type === "spellcastingEntry") .map((i) => i._id); const formulaIds = this.actor.system.formulas; src_logger.debug("ids", { moneyIds, deityIds, classIds, backgroundIds, heritageIds, ancestryIds, treasureIds, featIds, actionIds, equipmentIds, weaponIds, armorIds, loreIds, spellIds, formulaIds, }); // eslint-disable-next-line complexity const keepIds = this.actor.items.filter((i) => (!this.options.addMoney && moneyIds.includes(i._id)) || (!this.options.addClass && classIds.includes(i._id)) || (!this.options.addDeity && deityIds.includes(i._id)) || (!this.options.addBackground && backgroundIds.includes(i._id)) || (!this.options.addHeritage && heritageIds.includes(i._id)) || (!this.options.addAncestry && ancestryIds.includes(i._id)) || (!this.options.addTreasure && treasureIds.includes(i._id)) || (!this.options.addFeats && (featIds.includes(i._id) || actionIds.includes(i._id))) || (!this.options.addEquipment && equipmentIds.includes(i._id)) || (!this.options.addWeapons && weaponIds.includes(i._id)) || (!this.options.addArmor && armorIds.includes(i._id)) || (!this.options.addLores && loreIds.includes(i._id)) || (!this.options.addSpells && spellIds.includes(i._id)) ).map((i) => i._id); const deleteIds = this.actor.items.filter((i) => !keepIds.includes(i._id)).map((i) => i._id); src_logger.debug("ids", { deleteIds, keepIds, }); await this.actor.deleteEmbeddedDocuments("Item", deleteIds); } async #createAndUpdateItemsWithRuleRestore(items) { const ruleUpdates = []; const newItems = deepClone(items); for (const item of newItems) { if (item.system.rules?.length > 0) { ruleUpdates.push({ _id: item._id, system: { rules: deepClone(item.system.rules).map((r) => { delete r.choiceQueryResults; return r; }), }, }); item.system.rules = item.system.rules .filter((r) => { const excludedKeys = ["ActiveEffectLike", "AdjustModifier", "Resistance", "Strike"].includes(r.key); const grantItemWithFlags = ["GrantItem"].includes(r.key) && (hasProperty(r, "flag") || getProperty(r, "pathmuncherImport")); const objectSelection = ["ChoiceSet"].includes(r.key) && src_utils.isObject(r.selection); return !excludedKeys && !grantItemWithFlags && !objectSelection; }) .map((r) => { if (r.key === "ChoiceSet") { if ((src_utils.isString(r.choices) || src_utils.isObject(r.choices)) && r.choiceQueryResults) { r.choices = r.choiceQueryResults; } } if (r.pathmuncherImport) delete r.pathmuncherImport; return r; }); } } src_logger.debug("Creating items", newItems); await this.actor.createEmbeddedDocuments("Item", newItems, { keepId: true }); src_logger.debug("Rule updates", ruleUpdates); await this.actor.updateEmbeddedDocuments("Item", ruleUpdates); } async #updateItems(type) { src_logger.debug(`Updating ${type}`, this.result[type]); await this.actor.updateEmbeddedDocuments("Item", this.result[type]); } async #createActorEmbeddedDocuments() { this.#statusUpdate(1, 12, "Character", "Eating"); if (this.options.addDeity) await this.#createAndUpdateItemsWithRuleRestore(this.result.deity); if (this.options.addAncestry) await this.#createAndUpdateItemsWithRuleRestore(this.result.ancestry); if (this.options.addHeritage) await this.#createAndUpdateItemsWithRuleRestore(this.result.heritage); if (this.options.addBackground) await this.#createAndUpdateItemsWithRuleRestore(this.result.background); if (this.options.addClass) await this.#createAndUpdateItemsWithRuleRestore(this.result.class); if (this.options.addLores) await this.#createAndUpdateItemsWithRuleRestore(this.result.lores); const featNums = this.result.feats.length; if (this.options.addFeats) { for (const [i, feat] of this.result.feats.entries()) { // console.warn(`creating ${feat.name}`, feat); this.#statusUpdate(i, featNums, "Feats", "Eating"); await this.#createAndUpdateItemsWithRuleRestore([feat]); } } // if (this.options.addFeats) await this.#createAndUpdateItemsWithRuleRestore(this.result.feats); if (this.options.addSpells) { this.#statusUpdate(3, 12, "Spells", "Eating"); await this.#createAndUpdateItemsWithRuleRestore(this.result.casters); await this.#createAndUpdateItemsWithRuleRestore(this.result.spells); } this.#statusUpdate(4, 12, "Equipment", "Eating"); if (this.options.addEquipment) { await this.#createAndUpdateItemsWithRuleRestore(this.result.equipment); await this.#updateItems("equipment"); } if (this.options.addWeapons) await this.#createAndUpdateItemsWithRuleRestore(this.result.weapons); if (this.options.addArmor) { await this.#createAndUpdateItemsWithRuleRestore(this.result.armor); await this.#updateItems("armor"); } if (this.options.addTreasure) await this.#createAndUpdateItemsWithRuleRestore(this.result.treasure); if (this.options.addMoney) await this.#createAndUpdateItemsWithRuleRestore(this.result.money); } async #restoreEmbeddedRuleLogic() { const importedItems = this.actor.items.map((i) => i._id); // Loop back over items and add rule and item progression data back in. src_logger.debug("Restoring logic", { currentActor: duplicate(this.actor) }); const itemUpdates = []; for (const [key, value] of Object.entries(this.autoAddedFeatureItems)) { if (importedItems.includes(key)) { itemUpdates.push({ _id: `${key}`, system: { items: deepClone(value), }, }); } } this.#statusUpdate(1, 12, "Feats", "Clearing"); src_logger.debug("Restoring granted item logic", itemUpdates); await this.actor.updateEmbeddedDocuments("Item", itemUpdates); await this.actor.update({ "system.resources.focus": this.result.character.system.resources.focus, }); } async updateActor() { await this.#removeDocumentsToBeUpdated(); if (!this.options.addName) { delete this.result.character.name; delete this.result.character.prototypeToken.name; } if (!this.options.addFormulas) { delete this.result.character.system.formulas; } if (!this.boosts.custom) { setProperty(this.result.character, `system.abilities`, null); } src_logger.debug("Generated result", this.result); await this.actor.update(this.result.character); await this.#createActorEmbeddedDocuments(); await this.#restoreEmbeddedRuleLogic(); } async postImportCheck() { const badClass = this.options.addClass ? this.bad.filter((b) => b.type === "class").map((b) => `
${game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.MissingItemsOpen")}
${game.i18n.localize( `${constants.FLAG_NAME}.Dialogs.ResetSettings.Content` )}
`, buttons: { confirm: { icon: '', label: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Confirm`), callback: () => { resetSettings(); }, }, cancel: { icon: '', label: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Cancel`), }, }, default: "cancel", }); } } function registerSettings() { game.settings.registerMenu(constants.MODULE_NAME, "resetToDefaults", { name: `${constants.FLAG_NAME}.Settings.Reset.Title`, label: `${constants.FLAG_NAME}.Settings.Reset.Label`, hint: `${constants.FLAG_NAME}.Settings.Reset.Hint`, icon: "fas fa-refresh", type: ResetSettingsDialog, restricted: true, }); for (const [name, data] of Object.entries(constants.GET_DEFAULT_SETTINGS())) { game.settings.register(constants.MODULE_NAME, name, data); } game.settings.registerMenu(constants.MODULE_NAME, "selectCustomCompendiums", { name: `${constants.FLAG_NAME}.Settings.UseCustomCompendiumMappings.Title`, label: `${constants.FLAG_NAME}.Settings.UseCustomCompendiumMappings.Label`, hint: `${constants.FLAG_NAME}.Settings.UseCustomCompendiumMappings.Hint`, icon: "fas fa-book", type: CompendiumSelector, restricted: true, }); } ;// CONCATENATED MODULE: ./src/hooks/sheets.js function registerSheetButton() { const trustedUsersOnly = src_utils.setting("RESTRICT_TO_TRUSTED"); if (trustedUsersOnly && !game.user.isTrusted) return; /** * Character sheets */ const pcSheetNames = Object.values(CONFIG.Actor.sheetClasses.character) .map((sheetClass) => sheetClass.cls) .map((sheet) => sheet.name); pcSheetNames.forEach((sheetName) => { Hooks.on("render" + sheetName, (app, html, data) => { // only for GMs or the owner of this character if (!data.owner || !data.actor) return; const button = $(` ${constants.MODULE_FULL_NAME}`); button.click(() => { if (game.user.can("ACTOR_CREATE")) { const muncher = new PathmuncherImporter(PathmuncherImporter.defaultOptions, data.actor); muncher.render(true); } else { ui.notifications.warn(game.i18n.localize(`${constants.FLAG_NAME}.Notifications.CreateActorPermission`), { permanent: true }); } }); html.closest('.app').find('.pathmuncher-open').remove(); let titleElement = html.closest('.app').find('.window-title'); if (!app._minimized) button.insertAfter(titleElement); }); }); } ;// CONCATENATED MODULE: ./src/module.js Hooks.once("init", () => { registerSettings(); }); Hooks.once("ready", () => { registerSheetButton(); registerAPI(); }); /******/ })() ; //# sourceMappingURL=main.js.map