/******/ (() => { // 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", }, 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.ADD_VISION_FEATS]: { name: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Name`, hint: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Hint`, scope: "player", config: true, type: Boolean, default: true, }, // 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/data/equipment.js const SWAPS = [ /^(Greater) (.*)/, /^(Lesser) (.*)/, /^(Major) (.*)/, /^(Moderate) (.*)/, /^(Standard) (.*)/, ]; // this equipment is named differently in foundry vs pathbuilder const EQUIPMENT_RENAME_STATIC_MAP = [ { 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" }, ]; function generateDynamicNames(pbName) { const result = []; // if we have a hardcoded map, don't return here for (const reg of SWAPS) { const match = pbName.match(reg); if (match) { result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] }); } } 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" ]; ;// CONCATENATED MODULE: ./src/utils.js const utils = { 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, key); }, updateSetting: async (key, value) => { return game.settings.set(constants.MODULE_NAME, key, value); }, getFlags: (actor) => { const flags = actor.flags[constants.FLAG_NAME] ? actor.flags[constants.FLAG_NAME] : { pathbuilderId: undefined, addFeats: true, addEquipment: true, addBackground: true, addHeritage: true, addAncestry: true, addSpells: true, addMoney: true, addTreasure: true, addLores: true, addWeapons: true, addArmor: true, addDeity: true, addName: true, addClass: true, addFamiliars: true, addFormulas: true, askForChoices: false, }; 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; } }, }; /* harmony default export */ const src_utils = (utils); ;// CONCATENATED MODULE: ./src/data/features.js // these are features which are named differently in pathbuilder to foundry const POSTFIX_PB_REMOVALS = [ /(.*) (Racket)$/, /(.*) (Style)$/, ]; const PREFIX_PB_REMOVALS = [ /^(Arcane Thesis): (.*)/, /^(Arcane School): (.*)/, /^(The) (.*)/, ]; const PARENTHESIS = [ /^(.*) \((.*)\)$/, ]; const SPLITS = [ /^(.*): (.*)/, ]; const features_SWAPS = [ /^(Greater) (.*)/, /^(Lesser) (.*)/, /^(Major) (.*)/, ]; const FEAT_RENAME_STATIC_MAP = [ { 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: "Cave Climber Kobold", foundryName: "Caveclimber Kobold" }, { 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: "Constructed (Android)", foundryName: "Constructed" }, { 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: "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: "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: "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: "Paladin [Lawful Good]", foundryName: "Paladin" }, { pbName: "Parry", foundryName: "Aldori Parry" }, { pbName: "Polymath", foundryName: "Polymath Muse" }, { pbName: "Precise Debilitation", foundryName: "Precise Debilitations" }, { pbName: "Quick Climber", foundryName: "Quick Climb" }, { 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: "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" }, { 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" }, ]; 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 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) { result.push({ pbName, foundryName: match[2], details: match[1] }); } } 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 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 IGNORED_FEATS_LIST = [ "Unarmored", "Simple Weapon Expertise", "Spellbook", "Energy Emanation", // pathbuilder does not pass through a type for this "Imprecise Sense", // this gets picked up and added by granted features ]; function IGNORED_FEATS() { const visionFeats = src_utils.setting(constants.SETTINGS.ADD_VISION_FEATS) ? [] : ["Low-Light Vision", "Darkvision"]; return IGNORED_FEATS_LIST.concat(visionFeats); } ;// CONCATENATED MODULE: ./src/logger.js const logger = { _showMessage: (logLevel, data) => { if (!logLevel || !data || typeof logLevel !== "string") { return false; } const setting = src_utils.setting(constants.SETTINGS.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/app/Pathmuncher.js /* eslint-disable no-await-in-loop */ /* eslint-disable no-continue */ class Pathmuncher { // eslint-disable-next-line class-methods-use-this EQUIPMENT_RENAME_MAP(name) { return EQUIPMENT_RENAME_MAP(name); } getFoundryEquipmentName(pbName) { return this.EQUIPMENT_RENAME_MAP(pbName).find((map) => map.pbName == pbName)?.foundryName ?? pbName; } FEAT_RENAME_MAP(name) { const dynamicItems = [ { pbName: "Shining Oath", foundryName: `Shining Oath (${this.getChampionType()})` }, { 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 FEAT_RENAME_MAP(name).concat(dynamicItems); } getFoundryFeatureName(pbName) { const match = this.FEAT_RENAME_MAP(pbName).find((map) => map.pbName == pbName); return match ?? { pbName, foundryName: pbName, details: undefined }; } // eslint-disable-next-line class-methods-use-this get RESTRICTED_EQUIPMENT() { return RESTRICTED_EQUIPMENT; } // specials that are handled by Foundry and shouldn't be added // eslint-disable-next-line class-methods-use-this get IGNORED_FEATURES() { return IGNORED_FEATS(); }; // eslint-disable-next-line class-methods-use-this get IGNORED_EQUIPMENT() { return IGNORED_EQUIPMENT; }; getChampionType() { if (this.source.alignment == "LG") return "Paladin"; else if (this.source.alignment == "CG") return "Liberator"; else if (this.source.alignment == "NG") return "Redeemer"; else if (this.source.alignment == "LE") return "Tyrant"; else if (this.source.alignment == "CE") return "Antipaladin"; else if (this.source.alignment == "NE") return "Desecrator"; return "Unknown"; } constructor(actor, { addFeats = true, addEquipment = true, addSpells = true, addMoney = true, addLores = true, addWeapons = true, addArmor = true, addTreasure = true, addDeity = true, addName = true, addClass = true, addBackground = true, addHeritage = true, addAncestry = true, askForChoices = false } = {} ) { this.actor = actor; // note not all these options do anything yet! this.options = { addTreasure, addMoney, addFeats, addSpells, addEquipment, addLores, addWeapons, addArmor, addDeity, addName, addClass, addBackground, addHeritage, addAncestry, askForChoices, }; 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.allFeatureRules = {}; this.autoAddedFeatureRules = {}; this.grantItemLookUp = {}; this.autoFeats = []; 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 = []; } 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`)); } } getClassAdjustedSpecialNameLowerCase(name) { return `${name} (${this.source.class})`.toLowerCase(); } getDualClassAdjustedSpecialNameLowerCase(name) { return `${name} (${this.source.dualClass})`.toLowerCase(); } getAncestryAdjustedSpecialNameLowerCase(name) { return `${name} (${this.source.ancestry})`.toLowerCase(); } getHeritageAdjustedSpecialNameLowerCase(name) { return `${name} (${this.source.heritage})`.toLowerCase(); } 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 { return null; } } #generateFoundryFeatLocation(document, feature) { if (feature.type && feature.level) { const ancestryParagonVariant = game.settings.get("pf2e", "ancestryParagonVariant"); const dualClassVariant = game.settings.get("pf2e", "dualClassVariant"); // const freeArchetypeVariant = game.settings.get("pf2e", "freeArchetypeVariant"); const location = Pathmuncher.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, location, feature }); // eslint-disable-next-line max-depth if (ancestryParagonVariant && feature.type === "Ancestry Feat") { document.system.location = "ancestry-bonus"; this.usedLocationsAlternateRules.add(location); } else if (dualClassVariant && 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; } } #nameMap() { src_logger.debug("Starting Equipment Rename"); this.source.equipment .filter((e) => e[0] && e[0] !== "undefined") .forEach((e) => { const name = this.getFoundryEquipmentName(e[0]); const item = { pbName: name, qty: e[1], added: false }; this.parsed.equipment.push(item); }); this.source.armor .filter((e) => e && e !== "undefined") .forEach((e) => { const name = this.getFoundryEquipmentName(e.name); const item = mergeObject({ pbName: name, originalName: e.name, added: false }, e); this.parsed.armor.push(item); }); this.source.weapons .filter((e) => e && e !== "undefined") .forEach((e) => { const name = this.getFoundryEquipmentName(e.name); const item = mergeObject({ pbName: name, originalName: e.name, added: false }, e); this.parsed.weapons.push(item); }); src_logger.debug("Finished Equipment Rename"); src_logger.debug("Starting Special Rename"); this.source.specials .filter((special) => special && special !== "undefined" && special !== "Not Selected" && special !== this.source.heritage ) .forEach((special) => { const name = this.getFoundryFeatureName(special).foundryName; if (!this.#processSpecialData(name) && !this.IGNORED_FEATURES.includes(name)) { this.parsed.specials.push({ name, originalName: special, added: false }); } }); 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: false, type: feat[2], level: feat[3], originalName: feat[0], }; this.parsed.feats.push(data); }); src_logger.debug("Finished Feat Rename"); } #prepare() { this.#nameMap(); } static getSizeValue(size) { switch (size) { case 0: return "tiny"; case 1: return "sm"; case 3: return "lg"; default: return "med"; } } 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); } async #addDualClass(klass) { if (!game.settings.get("pf2e", "dualClassVariant")) { if (this.source.dualClass && this.source.dualClass !== "") { src_logger.warn(`Imported character is dual class but system is not configured for dual class`, { class: this.source.class, dualClass: this.source.dualClass, }); ui.notifications.warn(`Imported character is dual class but system is not configured for dual class`); } 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; } // find the dual class const compendium = await game.packs.get("pf2e.classes"); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); const foundryName = this.getFoundryFeatureName(this.source.dualClass).foundryName; const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(foundryName)) ?? index.find((i) => i.system.slug === game.pf2e.system.sluggify(this.source.dualClass)); if (!indexMatch) return; const doc = await compendium.getDocument(indexMatch._id); const dualClass = doc.toObject(); src_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(compendiumLabel, name, target) { src_logger.debug(`Checking for compendium documents for ${name} (${target}) in ${compendiumLabel}`); const compendium = await game.packs.get(compendiumLabel); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); const foundryName = this.getFoundryFeatureName(name).foundryName; const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(foundryName)) ?? index.find((i) => i.system.slug === game.pf2e.system.sluggify(name)); if (indexMatch) { const doc = await compendium.getDocument(indexMatch._id); const itemData = doc.toObject(); if (target === "class") { itemData.system.keyAbility.selected = this.source.keyability; await this.#addDualClass(itemData); } itemData._id = foundry.utils.randomID(); this.#generateGrantItemData(itemData); this.result[target].push(itemData); await this.#addGrantedItems(itemData); 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" // } #parsedFeatureMatch(type, slug, ignoreAdded) { // console.warn(`Trying to find ${slug} in ${type}, ignoreAdded? ${ignoreAdded}`); const parsedMatch = this.parsed[type].find((f) => (!ignoreAdded || (ignoreAdded && !f.added)) && ( slug === game.pf2e.system.sluggify(f.name) || slug === game.pf2e.system.sluggify(this.getClassAdjustedSpecialNameLowerCase(f.name)) || slug === game.pf2e.system.sluggify(this.getAncestryAdjustedSpecialNameLowerCase(f.name)) || slug === game.pf2e.system.sluggify(this.getHeritageAdjustedSpecialNameLowerCase(f.name)) || slug === game.pf2e.system.sluggify(f.originalName) || slug === game.pf2e.system.sluggify(this.getClassAdjustedSpecialNameLowerCase(f.originalName)) || slug === game.pf2e.system.sluggify(this.getAncestryAdjustedSpecialNameLowerCase(f.originalName)) || slug === game.pf2e.system.sluggify(this.getHeritageAdjustedSpecialNameLowerCase(f.originalName)) || (game.settings.get("pf2e", "dualClassVariant") && (slug === game.pf2e.system.sluggify(this.getDualClassAdjustedSpecialNameLowerCase(f.name)) || slug === game.pf2e.system.sluggify(this.getDualClassAdjustedSpecialNameLowerCase(f.originalName)) ) ) ) ); // console.warn(`Results of find ${slug} in ${type}, ignoreAdded? ${ignoreAdded}`, { // slug, // parsedMatch, // parsed: duplicate(this.parsed), // }); return parsedMatch; } #generatedResultMatch(type, slug) { const featMatch = this.result[type].find((f) => slug === f.system.slug); return featMatch; } #findAllFeatureMatch(slug, ignoreAdded) { const featMatch = this.#parsedFeatureMatch("feats", slug, ignoreAdded); if (featMatch) return featMatch; const specialMatch = this.#parsedFeatureMatch("specials", slug, ignoreAdded); 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) { src_logger.debug(`Adding granted item flags to ${document.name} (parent ${parent.name})`); const camelCase = game.pf2e.system.sluggify(document.system.slug, { camel: "dromedary" }); 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); if (!this.options.askForChoices) { this.result.feats.push(document); } const featureMatch = this.#findAllFeatureMatch(document.system.slug, true) ?? (document.name.includes("(") ? this.#findAllFeatureMatch(game.pf2e.system.sluggify(document.name.split("(")[0].trim()), true) : undefined ); // console.warn(`Matching feature for ${document.name}?`, { // featureMatch, // }); if (featureMatch) { if (hasProperty(featureMatch, "added")) { featureMatch.added = true; 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 }); } async #featureChoiceMatch(choices, ignoreAdded, adjustName) { for (const choice of choices) { const doc = adjustName ? game.i18n.localize(choice.label) : await fromUuid(choice.value); if (!doc) continue; const slug = adjustName ? game.pf2e.system.sluggify(doc) : doc.system.slug; const featMatch = this.#findAllFeatureMatch(slug, ignoreAdded); if (featMatch) { if (adjustName && hasProperty(featMatch, "added")) featMatch.added = true; src_logger.debug("Choices evaluated", { choices, document, featMatch, choice }); return choice; } } return undefined; } async #evaluateChoices(document, choiceSet) { src_logger.debug(`Evaluating choices for ${document.name}`, { document, choiceSet }); const tempActor = await this.#generateTempActor(); const cleansedChoiceSet = deepClone(choiceSet); try { const item = tempActor.getEmbeddedDocument("Item", document._id); const choiceSetRules = new game.pf2e.RuleElements.all.ChoiceSet(cleansedChoiceSet, item); const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat(); const choices = (await choiceSetRules.inflateChoices()).filter((c) => !c.predicate || c.predicate.test(rollOptions)); src_logger.debug("Starting choice evaluation", { document, choiceSet, item, choiceSetRules, rollOptions, choices, }); src_logger.debug("Evaluating choiceset", cleansedChoiceSet); const choiceMatch = await this.#featureChoiceMatch(choices, true, cleansedChoiceSet.adjustName); src_logger.debug("choiceMatch result", choiceMatch); if (choiceMatch) return choiceMatch; if (typeof cleansedChoiceSet.choices === "string" || Array.isArray(choices)) { for (const choice of choices) { const featMatch = this.#findAllFeatureMatch(choice.value, true, cleansedChoiceSet.adjustName); if (featMatch) { src_logger.debug("Choices evaluated", { cleansedChoiceSet, choices, document, featMatch, choice }); featMatch.added = true; choice.nouuid = true; return choice; } } } } 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(source, propertyData) { if (source === null || typeof source === "number" || (typeof source === "string" && !source.includes("{"))) { return source; } // Walk the object tree and resolve any string values found if (Array.isArray(source)) { for (let i = 0; i < source.length; i++) { source[i] = this.#resolveInjectedUuid(source[i]); } } else if (typeof source === 'object' && source !== null) { for (const [key, value] of Object.entries(source)) { if (typeof value === "string" || (typeof value === 'object' && value !== null)) { source[key] = this.#resolveInjectedUuid(value); } } return source; } else if (typeof source === "string") { const match = source.match(/{(actor|item|rule)\|(.*?)}/); if (match && match[1] === "actor") { return String(getProperty(this.result.character, match[1])); } else if (match) { const value = this.grantItemLookUp[match[0]].uuid; if (!value) { src_logger.error("Failed to resolve injected property", { source, propertyData, key: match[1], prop: match[2], }); } return String(value); } else { src_logger.error("Failed to resolve injected property", { source, propertyData, }); } } return source; } async #generateGrantItemData(document) { src_logger.debug(`Generating grantItem rule lookups for ${document.name}...`, { document: deepClone(document) }); for (const rule of document.system.rules.filter((r) => r.key === "GrantItem" && r.uuid.includes("{"))) { src_logger.debug("Generating rule for...", { document: deepClone(document), rule }); const match = rule.uuid.match(/{(item|rule)\|(.*?)}/); if (match) { const flagName = match[2].split(".").pop(); const choiceSet = document.system.rules.find((rule) => rule.key === "ChoiceSet" && rule.flag === flagName) ?? document.system.rules.find((rule) => rule.key === "ChoiceSet"); const choice = choiceSet ? (await this.#evaluateChoices(document, choiceSet)) : undefined; const value = choice?.value ?? undefined; if (!value) { src_logger.warn("Failed to resolve injected uuid", { ruleData: choiceSet, flagName, key: match[1], prop: match[2], value, }); } else { src_logger.debug(`Generated lookup ${value} for key ${document.name}`); } this.grantItemLookUp[rule.uuid] = { docId: document.id, key: rule.uuid, choice, uuid: value, flag: flagName, choiceSet, }; this.grantItemLookUp[`${document._id}-${flagName}`] = { docId: document.id, key: rule.uuid, choice, uuid: value, flag: flagName, choiceSet, }; this.grantItemLookUp[`${document._id}`] = { docId: document.id, key: rule.uuid, choice, uuid: value, flag: flagName, choiceSet, }; this.grantItemLookUp[`${document._id}-${flagName}`] = { docId: document.id, key: rule.uuid, choice, uuid: value, flag: flagName, choiceSet, }; } else { src_logger.error("Failed to resolve injected uuid", { document, rule, }); } } } async #checkRule(document, rule) { const tempActor = await this.#generateTempActor([document]); const cleansedRule = deepClone(rule); try { const item = tempActor.getEmbeddedDocument("Item", document._id); const ruleElement = cleansedRule.key === "ChoiceSet" ? new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, item) : new game.pf2e.RuleElements.all.GrantItem(cleansedRule, item); const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat(); const choices = cleansedRule.key === "ChoiceSet" ? (await ruleElement.inflateChoices()).filter((c) => !c.predicate || c.predicate.test(rollOptions)) : [ruleElement.resolveValue()]; const isGood = cleansedRule.key === "ChoiceSet" ? (await this.#featureChoiceMatch(choices, false)) !== undefined : ruleElement.test(rollOptions); 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]); } } // eslint-disable-next-line complexity async #addGrantedRules(document) { 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 = document.system.rules.filter((r) => !["GrantItem", "ChoiceSet", "MartialProficiency"].includes(r.key)); const rulesToKeep = []; this.allFeatureRules[document._id] = deepClone(document.system.rules); this.autoAddedFeatureRules[document._id] = deepClone(document.system.rules.filter((r) => !["GrantItem", "ChoiceSet"].includes(r.key))); await this.#generateGrantItemData(document); const grantRules = document.system.rules.filter((r) => r.key === "GrantItem"); const choiceRules = document.system.rules.filter((r) => r.key === "ChoiceSet"); for (const ruleTypes of [choiceRules, grantRules]) { for (const rawRuleEntry of ruleTypes) { const ruleEntry = deepClone(rawRuleEntry); src_logger.debug(`Checking ${document.name} rule key: ${ruleEntry.key}`); const lookupName = ruleEntry.flag ? `${document._id}-${ruleEntry.flag}` : document._id; src_logger.debug("Rule check, looking up", { id: `${document._id}-${ruleEntry.flag}`, lookup: this.grantItemLookUp[lookupName], lookups: this.grantItemLookUp, ruleEntry, lookupName, }); // have we pre-evaluated this choice? const choice = ruleEntry.key === "ChoiceSet" ? this.grantItemLookUp[lookupName]?.choice ? this.grantItemLookUp[lookupName].choice : await this.#evaluateChoices(document, ruleEntry) : undefined; const uuid = ruleEntry.key === "GrantItem" ? await this.#resolveInjectedUuid(ruleEntry.uuid, ruleEntry) : choice?.value; src_logger.debug(`UUID for ${document.name}: "${uuid}"`, document, ruleEntry, choice); const ruleFeature = uuid ? await fromUuid(uuid) : undefined; if (ruleFeature) { const featureDoc = ruleFeature.toObject(); featureDoc._id = foundry.utils.randomID(); if (featureDoc.system.rules) this.allFeatureRules[document._id] = deepClone(document.system.rules); setProperty(featureDoc, "flags.pathmuncher.origin.uuid", 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); continue; } src_logger.debug(`Found rule feature ${featureDoc.name} for ${document.name} for`, ruleEntry); if (ruleEntry.predicate) { const testResult = await this.#checkRule(featureDoc, ruleEntry); // eslint-disable-next-line max-depth if (!testResult) { const data = { document, ruleEntry, featureDoc, testResult }; src_logger.debug(`The test failed for ${document.name} rule key: ${ruleFeature.key} (This is probably not a problem).`, data); continue; } } if (choice) { ruleEntry.selection = choice.value; } this.autoAddedFeatureIds.add(`${ruleFeature.id}${ruleFeature.type}`); featureDoc._id = foundry.utils.randomID(); this.#createGrantedItem(featureDoc, document); if (hasProperty(ruleFeature, "system.rules.length")) await this.#addGrantedRules(featureDoc); } else if (choice?.nouuid) { src_logger.debug("Parsed no id rule", { choice, uuid, ruleEntry }); if (!ruleEntry.flag) ruleEntry.flag = game.pf2e.system.sluggify(document.name, { camel: "dromedary" }); ruleEntry.selection = choice.value; if (choice.label) document.name = `${document.name} (${choice.label})`; } else if (choice && uuid && !hasProperty(ruleEntry, "selection")) { src_logger.debug("Parsed odd choice rule", { choice, uuid, ruleEntry }); if (!ruleEntry.flag) ruleEntry.flag = game.pf2e.system.sluggify(document.name, { camel: "dromedary" }); ruleEntry.selection = choice.value; if (ruleEntry.adjustName && choice.label) { const label = game.i18n.localize(choice.label); const name = `${document.name} (${label})`; const pattern = (() => { const escaped = RegExp.escape(label); return new RegExp(`\\(${escaped}\\) \\(${escaped}\\)$`); })(); document.name = name.replace(pattern, `(${label})`); } } else { const data = { uuid: ruleEntry.uuid, document, ruleEntry, choice, lookup: this.grantItemLookUp[ruleEntry.uuid], }; if (ruleEntry.key === "GrantItem" && this.grantItemLookUp[ruleEntry.uuid]) { rulesToKeep.push(ruleEntry); // const lookup = this.grantItemLookUp[ruleEntry.uuid].choiceSet // eslint-disable-next-line max-depth // if (!rulesToKeep.some((r) => r.key == lookup && r.prompt === lookup.prompt)) { // rulesToKeep.push(this.grantItemLookUp[ruleEntry.uuid].choiceSet); // } } else if (ruleEntry.key === "ChoiceSet" && !hasProperty(ruleEntry, "flag")) { src_logger.debug("Prompting user for choices", ruleEntry); rulesToKeep.push(ruleEntry); } src_logger.warn("Unable to determine granted rule feature, needs better parser", data); } this.autoAddedFeatureRules[document._id].push(ruleEntry); } } if (!this.options.askForChoices) { // eslint-disable-next-line require-atomic-updates document.system.rules = rulesToKeep; } } async #addGrantedItems(document) { 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)) { src_logger.debug(`Checking granted item ${document.name}, 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); if (hasProperty(featureDoc, "system.rules")) await this.#addGrantedRules(featureDoc); } if (!this.options.askForChoices) { // eslint-disable-next-line require-atomic-updates document.system.items = failedFeatureItems; } } if (hasProperty(document, "system.rules")) await this.#addGrantedRules(document); } async #detectGrantedFeatures() { if (this.result.class.length > 0) await this.#addGrantedItems(this.result.class[0]); if (this.result.ancestry.length > 0) await this.#addGrantedItems(this.result.ancestry[0]); if (this.result.heritage.length > 0) await this.#addGrantedItems(this.result.heritage[0]); if (this.result.background.length > 0) await this.#addGrantedItems(this.result.background[0]); } 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); setProperty(this.result.character, "system.details.keyability.value", this.source.keyability); if (this.source.deity !== "Not set") setProperty(this.result.character, "system.details.deity.value", this.source.deity); setProperty(this.result.character, "system.traits.size.value", Pathmuncher.getSizeValue(this.source.size)); setProperty(this.result.character, "system.traits.languages.value", this.source.languages.map((l) => l.toLowerCase())); this.#processSenses(); setProperty(this.result.character, "system.abilities.str.value", this.source.abilities.str); setProperty(this.result.character, "system.abilities.dex.value", this.source.abilities.dex); setProperty(this.result.character, "system.abilities.con.value", this.source.abilities.con); setProperty(this.result.character, "system.abilities.int.value", this.source.abilities.int); setProperty(this.result.character, "system.abilities.wis.value", this.source.abilities.wis); setProperty(this.result.character, "system.abilities.cha.value", this.source.abilities.cha); setProperty(this.result.character, "system.saves.fortitude.tank", this.source.proficiencies.fortitude / 2); setProperty(this.result.character, "system.saves.reflex.value", this.source.proficiencies.reflex / 2); setProperty(this.result.character, "system.saves.will.value", this.source.proficiencies.will / 2); setProperty(this.result.character, "system.martial.advanced.rank", this.source.proficiencies.advanced / 2); setProperty(this.result.character, "system.martial.heavy.rank", this.source.proficiencies.heavy / 2); setProperty(this.result.character, "system.martial.light.rank", this.source.proficiencies.light / 2); setProperty(this.result.character, "system.martial.medium.rank", this.source.proficiencies.medium / 2); setProperty(this.result.character, "system.martial.unarmored.rank", this.source.proficiencies.unarmored / 2); setProperty(this.result.character, "system.martial.martial.rank", this.source.proficiencies.martial / 2); setProperty(this.result.character, "system.martial.simple.rank", this.source.proficiencies.simple / 2); setProperty(this.result.character, "system.martial.unarmed.rank", this.source.proficiencies.unarmed / 2); setProperty(this.result.character, "system.skills.acr.rank", this.source.proficiencies.acrobatics / 2); setProperty(this.result.character, "system.skills.arc.rank", this.source.proficiencies.arcana / 2); setProperty(this.result.character, "system.skills.ath.rank", this.source.proficiencies.athletics / 2); setProperty(this.result.character, "system.skills.cra.rank", this.source.proficiencies.crafting / 2); setProperty(this.result.character, "system.skills.dec.rank", this.source.proficiencies.deception / 2); setProperty(this.result.character, "system.skills.dip.rank", this.source.proficiencies.diplomacy / 2); setProperty(this.result.character, "system.skills.itm.rank", this.source.proficiencies.intimidation / 2); setProperty(this.result.character, "system.skills.med.rank", this.source.proficiencies.medicine / 2); setProperty(this.result.character, "system.skills.nat.rank", this.source.proficiencies.nature / 2); setProperty(this.result.character, "system.skills.occ.rank", this.source.proficiencies.occultism / 2); setProperty(this.result.character, "system.skills.prf.rank", this.source.proficiencies.performance / 2); setProperty(this.result.character, "system.skills.rel.rank", this.source.proficiencies.religion / 2); setProperty(this.result.character, "system.skills.soc.rank", this.source.proficiencies.society / 2); setProperty(this.result.character, "system.skills.ste.rank", this.source.proficiencies.stealth / 2); setProperty(this.result.character, "system.skills.sur.rank", this.source.proficiencies.survival / 2); setProperty(this.result.character, "system.skills.thi.rank", this.source.proficiencies.thievery / 2); 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) => i.system.slug === game.pf2e.system.sluggify(name) || i.system.slug === game.pf2e.system.sluggify(this.getClassAdjustedSpecialNameLowerCase(name)) || i.system.slug === game.pf2e.system.sluggify(this.getAncestryAdjustedSpecialNameLowerCase(name)) || i.system.slug === game.pf2e.system.sluggify(this.getHeritageAdjustedSpecialNameLowerCase(name)) || (game.settings.get("pf2e", "dualClassVariant") && (i.system.slug === game.pf2e.system.sluggify(this.getDualClassAdjustedSpecialNameLowerCase(name)) ) ) ); if (indexMatch) return indexMatch; } return undefined; } async #generateFeatItems(compendiumLabel) { const compendium = await game.packs.get(compendiumLabel); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); 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; } } return 0; }); for (const featArray of [this.parsed.feats, this.parsed.specials]) { for (const pBFeat of featArray) { if (pBFeat.added) continue; src_logger.debug("Generating feature for", pBFeat); const indexMatch = this.#indexFind(index, [pBFeat.name, pBFeat.originalName]); const displayName = pBFeat.extra ? `${pBFeat.name} (${pBFeat.extra})` : pBFeat.name; if (!indexMatch) { src_logger.debug(`Unable to match feat ${displayName}`, { displayName, name: pBFeat.name, extra: pBFeat.extra, pBFeat, compendiumLabel }); this.check[pBFeat.originalName] = { name: displayName, type: "feat", details: { displayName, name: pBFeat.name, originalName: pBFeat.originalName, extra: pBFeat.extra, pBFeat, compendiumLabel } }; 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, compendiumLabel }); continue; } const doc = await compendium.getDocument(indexMatch._id); const item = doc.toObject(); item._id = foundry.utils.randomID(); item.name = displayName; this.#generateFoundryFeatLocation(item, pBFeat); this.result.feats.push(item); await this.#addGrantedItems(item); } } } async #generateSpecialItems(compendiumLabel) { const compendium = await game.packs.get(compendiumLabel); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); for (const special of this.parsed.specials) { if (special.added) continue; src_logger.debug("Generating special for", special); const indexMatch = this.#indexFind(index, [special.name, special.originalName]); if (!indexMatch) { src_logger.debug(`Unable to match special ${special.name}`, { special: special.name, compendiumLabel }); 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, compendiumLabel }); continue; } const doc = await compendium.getDocument(indexMatch._id); const docData = doc.toObject(); docData._id = foundry.utils.randomID(); this.result.feats.push(docData); await this.#addGrantedItems(docData); } } async #generateEquipmentItems(pack = "pf2e.equipment-srd") { const compendium = game.packs.get(pack); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); const compendiumBackpack = await compendium.getDocument("3lgwjrFEsQVKzhh7"); const adventurersPack = this.parsed.equipment.find((e) => e.pbName === "Adventurer's Pack"); const backpackInstance = adventurersPack ? compendiumBackpack.toObject() : null; if (backpackInstance) { adventurersPack.added = true; backpackInstance._id = foundry.utils.randomID(); 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 compendium.getDocument(indexMatch._id); const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); itemData.system.quantity = content.qty; itemData.system.containerId = backpackInstance?._id; this.result.equipment.push(itemData); } } for (const e of this.parsed.equipment) { if (e.pbName === "Adventurer's Pack") continue; if (e.added) continue; if (this.IGNORED_EQUIPMENT.includes(e.pbName)) { e.added = true; continue; } src_logger.debug("Generating item for", e); const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(e.pbName)); 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 compendium.getDocument(indexMatch._id); if (doc.type != "kit") { const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); itemData.system.quantity = e.qty; const type = doc.type === "treasure" ? "treasure" : "equipment"; this.result[type].push(itemData); } // eslint-disable-next-line require-atomic-updates e.added = true; } } async #generateWeaponItems() { const compendium = game.packs.get("pf2e.equipment-srd"); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); for (const w of this.parsed.weapons) { if (this.IGNORED_EQUIPMENT.includes(w.pbName)) { w.added = true; continue; } src_logger.debug("Generating weapon for", w); const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(w.pbName)); if (!indexMatch) { src_logger.error(`Unable to match weapon item ${w.name}`, w); this.bad.push({ pbName: w.pbName, type: "weapon", details: { w } }); continue; } const doc = await compendium.getDocument(indexMatch._id); const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); itemData.system.quantity = w.qty; itemData.system.damage.die = w.die; itemData.system.potencyRune.value = w.pot; itemData.system.strikingRune.value = w.str; if (w.runes[0]) itemData.system.propertyRune1.value = game.pf2e.system.sluggify(w.runes[0], { camel: "dromedary" }); if (w.runes[1]) itemData.system.propertyRune2.value = game.pf2e.system.sluggify(w.runes[1], { camel: "dromedary" }); if (w.runes[2]) itemData.system.propertyRune3.value = game.pf2e.system.sluggify(w.runes[2], { camel: "dromedary" }); if (w.runes[3]) itemData.system.propertyRune4.value = game.pf2e.system.sluggify(w.runes[3], { camel: "dromedary" }); if (w.mat) { const material = w.mat.split(" (")[0]; itemData.system.preciousMaterial.value = game.pf2e.system.sluggify(material, { camel: "dromedary" }); itemData.system.preciousMaterialGrade.value = Pathmuncher.getMaterialGrade(w.mat); } if (w.display) itemData.name = w.display; this.result.weapons.push(itemData); w.added = true; } } async #generateArmorItems() { const compendium = game.packs.get("pf2e.equipment-srd"); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); for (const a of this.parsed.armor) { if (this.IGNORED_EQUIPMENT.includes(a.pbName)) { a.added = true; continue; } src_logger.debug("Generating armor for", a); const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(a.pbName) || i.system.slug === game.pf2e.system.sluggify(`${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 compendium.getDocument(indexMatch._id); const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); itemData.system.equipped.value = a.worn ?? false; if (!this.RESTRICTED_EQUIPMENT.some((i) => itemData.name.startsWith(i))) { itemData.system.equipped.inSlot = a.worn ?? false; itemData.system.quantity = a.qty; itemData.system.category = a.prof; itemData.system.potencyRune.value = a.pot; itemData.system.resiliencyRune.value = a.res; const isShield = itemData.system.category === "shield"; itemData.system.equipped.handsHeld = isShield && a.worn ? 1 : 0; itemData.system.equipped.carryType = isShield && a.worn ? "held" : "worn"; if (a.runes[0]) itemData.system.propertyRune1.value = game.pf2e.system.sluggify(a.runes[0], { camel: "dromedary" }); if (a.runes[1]) itemData.system.propertyRune2.value = game.pf2e.system.sluggify(a.runes[1], { camel: "dromedary" }); if (a.runes[2]) itemData.system.propertyRune3.value = game.pf2e.system.sluggify(a.runes[2], { camel: "dromedary" }); if (a.runes[3]) itemData.system.propertyRune4.value = game.pf2e.system.sluggify(a.runes[3], { camel: "dromedary" }); if (a.mat) { const material = a.mat.split(" (")[0]; itemData.system.preciousMaterial.value = game.pf2e.system.sluggify(material, { camel: "dromedary" }); itemData.system.preciousMaterialGrade.value = Pathmuncher.getMaterialGrade(a.mat); } } if (a.display) itemData.name = a.display; this.result.armor.push(itemData); // eslint-disable-next-line require-atomic-updates 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"; // 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; // final fallback // 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"; } async #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: { slot0: { max: caster.perDay[0], prepared: {}, value: caster.perDay[0], }, slot1: { max: caster.perDay[1], prepared: {}, value: caster.perDay[1], }, slot2: { max: caster.perDay[2], prepared: {}, value: caster.perDay[2], }, slot3: { max: caster.perDay[3], prepared: {}, value: caster.perDay[3], }, slot4: { max: caster.perDay[4], prepared: {}, value: caster.perDay[4], }, slot5: { max: caster.perDay[5], prepared: {}, value: caster.perDay[5], }, slot6: { max: caster.perDay[6], prepared: {}, value: caster.perDay[6], }, slot7: { max: caster.perDay[7], prepared: {}, value: caster.perDay[7], }, slot8: { max: caster.perDay[8], prepared: {}, value: caster.perDay[8], }, slot9: { max: caster.perDay[9], prepared: {}, value: caster.perDay[9], }, slot10: { max: caster.perDay[10], prepared: {}, value: caster.perDay[10], }, }, showUnpreparedSpells: { value: true }, }; const data = { _id: foundry.utils.randomID(), name, type: "spellcastingEntry", system: spellcastingEntity, }; this.result.casters.push(data); return data; } async #processSpells() { const compendium = game.packs.get("pf2e.spells-srd"); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); const psiCompendium = game.packs.get("pf2e-psychic-amps.psychic-psi-cantrips"); const psiIndex = psiCompendium ? await psiCompendium.getIndex({ fields: ["name", "type", "system.slug"] }) : undefined; 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 = await this.#generateSpellCaster(caster); src_logger.debug("Generated caster instance", instance); for (const spellSelection of caster.spells) { const level = spellSelection.spellLevel; for (const [i, spell] of spellSelection.list.entries()) { const spellName = spell.split("(")[0].trim(); src_logger.debug("spell details", { spell, spellName, spellSelection, list: spellSelection.list }); const psiMatch = psiIndex ? psiIndex.find((i) => i.name === spell) : undefined; const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(spellName)); if (!indexMatch && !psiMatch) { src_logger.error(`Unable to match spell ${spell}`, { spell, spellName, spellSelection, caster, instance }); this.bad.push({ pbName: spell, type: "spell", details: { originalName: spell, name: spellName, spellSelection, caster } }); continue; } const doc = psiMatch ? await psiCompendium.getDocument(psiMatch._id) : await compendium.getDocument(indexMatch._id); const itemData = doc.toObject(); itemData._id = foundry.utils.randomID(); itemData.system.location.heightenedLevel = level; itemData.system.location.value = instance._id; this.result.spells.push(itemData); instance.system.slots[`slot${level}`].prepared[i] = { id: itemData._id }; } } } setProperty(this.result.character, "system.resources.focus.max", this.result.focusPool); setProperty(this.result.character, "system.resources.focus.value", this.result.focusPool); } 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 compendium = game.packs.get("pf2e.equipment-srd"); const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] }); const uuids = []; for (const formulaSource of this.source.formula) { for (const formulaName of formulaSource.known) { const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(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 compendium.getDocument(indexMatch._id); uuids.push({ uuid: doc.uuid }); } } setProperty(this.result.character, "system.crafting.formulas", uuids); } async #processFeats() { await this.#generateFeatItems("pf2e.feats-srd"); await this.#generateFeatItems("pf2e.ancestryfeatures"); await this.#generateSpecialItems("pf2e.ancestryfeatures"); await this.#generateSpecialItems("pf2e.classfeatures"); await this.#generateSpecialItems("pf2e.actionspf2e"); } async #processEquipment() { await this.#generateEquipmentItems(); await this.#generateWeaponItems(); await this.#generateArmorItems(); await this.#generateMoney(); } async #generateTempActor(documents = []) { const actorData = mergeObject({ type: "character" }, this.result.character); actorData.name = "Mr Temp"; const actor = await Actor.create(actorData); const currentState = duplicate(this.result); const currentItems = [ ...(this.options.askForChoices ? this.autoFeats : []), ...currentState.feats, ...currentState.class, ...currentState.background, ...currentState.ancestry, ...currentState.heritage, ...currentState.deity, ...currentState.lores, ]; for (const doc of documents) { if (!currentItems.some((d) => d._id === doc._id)) { currentItems.push(doc); } } try { const items = duplicate(currentItems).map((i) => { if (i.system.items) i.system.items = []; if (i.system.rules) i.system.rules = []; return i; }); await actor.createEmbeddedDocuments("Item", items, { keepId: true }); const ruleIds = currentItems.map((i) => i._id); const ruleUpdates = []; for (const [key, value] of Object.entries(this.allFeatureRules)) { if (ruleIds.includes(key)) { ruleUpdates.push({ _id: key, system: { // rules: value, rules: value.filter((r) => ["GrantItem", "ChoiceSet", "RollOption"].includes(r.key)), }, }); } } // console.warn("rule updates", 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), }, }); } for (const doc of documents) { if (getProperty(doc, "system.rules")?.length > 0 && !ruleUpdates.some((r) => r._id === doc._id)) { ruleUpdates.push({ _id: doc._id, system: { rules: deepClone(doc.system.rules), }, }); } } 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; this.#prepare(); await this.#processCore(); await this.#processFormulas(); await this.#processGenericCompendiumLookup("pf2e.deities", this.source.deity, "deity"); await this.#processGenericCompendiumLookup("pf2e.backgrounds", this.source.background, "background"); await this.#processGenericCompendiumLookup("pf2e.classes", this.source.class, "class"); await this.#processGenericCompendiumLookup("pf2e.ancestries", this.source.ancestry, "ancestry"); await this.#processGenericCompendiumLookup("pf2e.heritages", this.source.heritage, "heritage"); await this.#detectGrantedFeatures(); await this.#processFeats(); await this.#processEquipment(); await this.#processSpells(); 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 #createActorEmbeddedDocuments() { if (this.options.addDeity) await this.actor.createEmbeddedDocuments("Item", this.result.deity, { keepId: true }); if (this.options.addAncestry) await this.actor.createEmbeddedDocuments("Item", this.result.ancestry, { keepId: true }); if (this.options.addHeritage) await this.actor.createEmbeddedDocuments("Item", this.result.heritage, { keepId: true }); if (this.options.addBackground) await this.actor.createEmbeddedDocuments("Item", this.result.background, { keepId: true }); if (this.options.addClass) await this.actor.createEmbeddedDocuments("Item", this.result.class, { keepId: true }); if (this.options.addLores) await this.actor.createEmbeddedDocuments("Item", this.result.lores, { keepId: true }); // for (const feat of this.result.feats.reverse()) { // console.warn(`creating ${feat.name}`, feat); // await this.actor.createEmbeddedDocuments("Item", [feat], { keepId: true }); // } if (this.options.addFeats) await this.actor.createEmbeddedDocuments("Item", this.result.feats, { keepId: true }); if (this.options.addSpells) { await this.actor.createEmbeddedDocuments("Item", this.result.casters, { keepId: true }); await this.actor.createEmbeddedDocuments("Item", this.result.spells, { keepId: true }); } if (this.options.addEquipment) await this.actor.createEmbeddedDocuments("Item", this.result.equipment, { keepId: true }); if (this.options.addWeapons) await this.actor.createEmbeddedDocuments("Item", this.result.weapons, { keepId: true }); if (this.options.addArmor) { await this.actor.createEmbeddedDocuments("Item", this.result.armor, { keepId: true }); await this.actor.updateEmbeddedDocuments("Item", this.result.armor, { keepId: true }); } if (this.options.addTreasure) await this.actor.createEmbeddedDocuments("Item", this.result.treasure, { keepId: true }); if (this.options.addMoney) await this.actor.createEmbeddedDocuments("Item", this.result.money, { keepId: true }); } async #restoreEmbeddedRuleLogic() { const importedItems = this.actor.items.map((i) => i._id); // Loop back over items and add rule and item progression data back in. if (!this.options.askForChoices) { src_logger.debug("Restoring logic", { currentActor: duplicate(this.actor) }); const ruleUpdates = []; for (const [key, value] of Object.entries(this.autoAddedFeatureRules)) { if (importedItems.includes(key)) { ruleUpdates.push({ _id: `${key}`, system: { rules: deepClone(value), }, }); } } src_logger.debug("Restoring rule logic", ruleUpdates); await this.actor.updateEmbeddedDocuments("Item", ruleUpdates); const itemUpdates = []; for (const [key, value] of Object.entries(this.autoAddedFeatureItems)) { if (importedItems.includes(key)) { itemUpdates.push({ _id: `${key}`, system: { items: deepClone(value), }, }); } } src_logger.debug("Restoring granted item logic", itemUpdates); await this.actor.updateEmbeddedDocuments("Item", itemUpdates); } } 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; } 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); } } ;// CONCATENATED MODULE: ./src/hooks/sheets.js function registerSheetButton() { const trustedUsersOnly = src_utils.setting(constants.SETTINGS.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 = $(` Pathmuncher`); button.click(() => { const muncher = new PathmuncherImporter(PathmuncherImporter.defaultOptions, data.actor); muncher.render(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