All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

4902 lines
182 KiB

/******/ (() => { // 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) => `<li>${game.i18n.localize("pathmuncher.Labels.Class")}: ${b.pbName}</li>`)
: [];
const badHeritage = this.options.addHeritage
? this.bad.filter((b) => b.type === "heritage").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Heritage")}: ${b.pbName}</li>`)
: [];
const badAncestry = this.options.addAncestry
? this.bad.filter((b) => b.type === "ancestry").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Ancestry")}: ${b.pbName}</li>`)
: [];
const badBackground = this.options.addBackground
? this.bad.filter((b) => b.type === "background").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Background")}: ${b.pbName}</li>`)
: [];
const badDeity = this.options.addDeity
? this.bad.filter((b) => b.type === "deity" && b.pbName !== "Not set" && b.pbName !== "").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Deity")}: ${b.pbName}</li>`)
: [];
const badFeats = this.options.addFeats
? this.bad.filter((b) => b.type === "feat").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Feats")}: ${b.pbName}</li>`)
: [];
const badFeats2 = this.options.addFeats
? Object.values(this.check).filter((b) =>
b.type === "feat"
&& this.parsed.feats.some((f) => f.name === b.details.name && !f.added)
).map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Feats")}: ${b.details.name}</li>`)
: [];
const badSpecials = this.options.addFeats
? Object.values(this.check).filter((b) =>
(b.type === "special")
&& this.parsed.specials.some((f) => f.name === b.details.name && !f.added)
).map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Specials")}: ${b.details.originalName}</li>`)
: [];
const badEquipment = this.options.addEquipment
? this.bad.filter((b) => b.type === "equipment").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Equipment")}: ${b.pbName}</li>`)
: [];
const badWeapons = this.options.addWeapons
? this.bad.filter((b) => b.type === "weapons").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Weapons")}: ${b.pbName}</li>`)
: [];
const badArmor = this.options.addArmor
? this.bad.filter((b) => b.type === "armor").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Armor")}: ${b.pbName}</li>`)
: [];
const badSpellcasting = this.options.addSpells
? this.bad.filter((b) => b.type === "spellcasting").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Spellcasting")}: ${b.pbName}</li>`)
: [];
const badSpells = this.options.addSpells
? this.bad.filter((b) => b.type === "spells").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Spells")}: ${b.pbName}</li>`)
: [];
const badFamiliars = this.options.addFamiliars
? this.bad.filter((b) => b.type === "familiars").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Familiars")}: ${b.pbName}</li>`)
: [];
const badFormulas = this.options.addFormulas
? this.bad.filter((b) => b.type === "formulas").map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Formulas")}: ${b.pbName}</li>`)
: [];
const totalBad = [
...badClass,
...badAncestry,
...badHeritage,
...badBackground,
...badDeity,
...badFeats,
...badFeats2,
...badSpecials,
...badEquipment,
...badWeapons,
...badArmor,
...badSpellcasting,
...badSpells,
...badFamiliars,
...badFormulas,
];
let warning = "";
if (totalBad.length > 0) {
warning += `<p>${game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.MissingItemsOpen")}</p><ul>${totalBad.join("\n")}</ul><br>`;
}
src_logger.debug("Bad thing check", {
badClass,
badAncestry,
badHeritage,
badBackground,
badDeity,
badFeats,
badFeats2,
badSpecials,
badEquipment,
badWeapons,
badArmor,
badSpellcasting,
badSpells,
badFamiliars,
badFormulas,
totalBad,
count: totalBad.length,
focusPool: this.result.focusPool,
warning,
});
if (totalBad.length > 0) {
ui.notifications.warn(game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.CompletedWithNotes"));
new Dialog({
title: game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.ImportNotes"),
content: warning,
buttons: {
yes: {
icon: "<i class='fas fa-check'></i>",
label: game.i18n.localize("pathmuncher.Labels.Finished"),
},
},
default: "yes",
}).render(true);
} else {
ui.notifications.info(game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.CompletedSuccess"));
}
}
}
;// CONCATENATED MODULE: ./src/app/PetShop.js
/* eslint-disable no-await-in-loop */
/* eslint-disable no-continue */
/**
* The PetShop class looks for familiars in a Pathmunch data set and creates/updates as appropriate.
*/
class PetShop {
constructor ({ type = "familiar", parent, pathbuilderJson } = {}) {
this.parent = parent;
this.pathbuilderJson = pathbuilderJson;
this.type = type;
this.result = {
pets: [],
features: {},
};
this.bad = {};
this.folders = {};
}
async ensureFolder(type) {
const folderName = game.i18n.localize(`${constants.FLAG_NAME}.Folders.${type}`);
this.folders[type] = await src_utils.getOrCreateFolder(this.parent.folder, "Actor", folderName);
}
async #existingPetCheck(petName, type) {
const existingPet = game.actors.find((a) =>
a.type === type.toLowerCase()
&& a.name === petName
&& a.system.master.id === this.parent._id
);
if (existingPet) return existingPet.toObject();
const actorData = {
type: type.toLowerCase(),
name: petName,
system: {
master: {
id: this.parent._id,
ability: this.parent.system.details.keyability.value,
},
},
prototypeToken: {
name: petName,
},
folder: this.folders[type].id,
};
const actor = await Actor.create(actorData);
return actor.toObject();
}
#buildCore(petData) {
setProperty(petData, "system.attributes.value", this.parent.system.details.level.value * 5);
return petData;
}
async #generatePetFeatures(pet, json) {
const compendium = game.packs.get("pf2e.familiar-abilities");
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
this.result.features[pet._id] = [];
this.bad[pet._id] = [];
for (const featureName of json.abilities) {
const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(featureName));
if (!indexMatch) {
src_logger.warn(`Unable to match pet feature ${featureName}`, { pet, json, name: featureName });
this.bad[pet._id].push({ pbName: featureName, type: "feature", details: { pet, json, name: featureName } });
continue;
}
const doc = (await compendium.getDocument(indexMatch._id)).toObject();
doc._id = foundry.utils.randomID();
this.result.features[pet._id].push(doc);
}
}
async buildPet(json) {
const name = json.name === json.type || !json.name.includes("(")
? `${this.parent.name}'s ${json.type}`
: json.name.split("(")[1].split(")")[0];
const petData = await this.#existingPetCheck(name, json.type);
const pet = this.#buildCore(petData);
await this.#generatePetFeatures(pet, json);
this.result.pets.push(pet);
}
async updatePets() {
for (const petData of this.result.pets) {
const actor = game.actors.get(petData._id);
await actor.deleteEmbeddedDocuments("Item", [], { deleteAll: true });
await actor.update(petData);
await actor.createEmbeddedDocuments("Item", this.result.features[petData._id], { keepId: true });
}
}
async processPets() {
const petData = this.type === "familiar" && this.pathbuilderJson.familiars
? this.pathbuilderJson.familiars
: this.pathbuilderJson.pets.filter((p) => this.type === p.type.toLowerCase());
await this.ensureFolder(src_utils.capitalize(this.type));
for (const petJson of petData) {
await this.buildPet(petJson);
}
await this.updatePets();
src_logger.debug("Pets", {
results: this.results,
bad: this.bad,
});
}
async addPetEffects() {
const features = [];
for (const petData of this.result.pets) {
for (const feature of this.result.features[petData._id].filter((f) => f.system.rules?.some((r) => r.key === "ActiveEffectLike"))) {
if (!this.parent.items.some((i) => i.type === "effect" && i.system.slug === feature.system.slug)) {
features.push(feature);
}
}
}
await this.parent.createEmbeddedDocuments("Item", features);
}
}
;// CONCATENATED MODULE: ./src/app/PathmuncherImporter.js
class PathmuncherImporter extends FormApplication {
constructor(options, actor) {
super(options);
this.actor = game.actors.get(actor.id ? actor.id : actor._id);
this.backup = duplicate(this.actor);
this.mode = "number";
}
static get defaultOptions() {
const options = super.defaultOptions;
options.title = game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.PathmuncherImporter.Title`);
options.template = `${constants.PATH}/templates/pathmuncher.hbs`;
options.classes = ["pathmuncher"];
options.id = "pathmuncher";
options.width = 400;
options.closeOnSubmit = false;
options.tabs = [{ navSelector: ".tabs", contentSelector: "form", initial: "number" }];
return options;
}
/** @override */
async getData() {
const flags = src_utils.getFlags(this.actor);
return {
flags,
id: flags?.pathbuilderId ?? "",
actor: this.actor,
};
}
/** @override */
activateListeners(html) {
super.activateListeners(html);
$("#pathmuncher").css("height", "auto");
$(html)
.find('.item')
.on("click", (event) => {
if (!event.target?.dataset?.tab) return;
this.mode = event.target.dataset.tab;
});
}
static _updateProgress(total, count, type, prefixLabel = "Cooking") {
const localizedType = game.i18n.localize(`pathmuncher.Labels.${type}`);
const progressBar = document.getElementById("pathmuncher-status");
progressBar.style.width = `${Math.trunc((count / total) * 100)}%`;
progressBar.innerHTML = `<span>${game.i18n.localize(`pathmuncher.Labels.${prefixLabel}`)} (${localizedType})...</span>`;
}
async _updateObject(event, formData) {
document.getElementById("pathmuncher-button").disabled = true;
const pathbuilderId = formData.textBoxBuildID;
const options = {
pathbuilderId,
addMoney: formData.checkBoxMoney,
addFeats: formData.checkBoxFeats,
addSpells: formData.checkBoxSpells,
adjustBlendedSlots: formData.checkBoxBlendedSlots,
addEquipment: formData.checkBoxEquipment,
addTreasure: formData.checkBoxTreasure,
addLores: formData.checkBoxLores,
addWeapons: formData.checkBoxWeapons,
addArmor: formData.checkBoxArmor,
addDeity: formData.checkBoxDeity,
addName: formData.checkBoxName,
addClass: formData.checkBoxClass,
addBackground: formData.checkBoxBackground,
addHeritage: formData.checkBoxHeritage,
addAncestry: formData.checkBoxAncestry,
addFamiliars: formData.checkBoxFamiliars,
addFormulas: formData.checkBoxFormulas,
statusCallback: PathmuncherImporter._updateProgress.bind(this),
};
src_logger.debug("Pathmuncher options", options);
await src_utils.setFlags(this.actor, options);
const statusBar = document.getElementById("pathmuncher-import-progress");
statusBar.classList.toggle("import-hidden");
const pathmuncher = new Pathmuncher(this.actor, options);
if (this.mode === "number") {
await pathmuncher.fetchPathbuilder(pathbuilderId);
} else if (this.mode === "json") {
try {
const jsonData = JSON.parse(formData.textBoxBuildJSON.trim());
pathmuncher.source = jsonData.build;
} catch (err) {
ui.notifications.error("Unable to parse JSON data");
return;
}
}
src_logger.debug("Pathmuncher Source", pathmuncher.source);
await pathmuncher.processCharacter();
src_logger.debug("Post processed character", pathmuncher);
await pathmuncher.updateActor();
src_logger.debug("Final import details", {
actor: this.actor,
pathmuncher,
options,
pathbuilderSource: pathmuncher.source,
pathbuilderId,
});
if (options.addFamiliars) {
const petShop = new PetShop({
type: "familiar",
parent: this.actor,
pathbuilderJson: pathmuncher.source
});
await petShop.processPets();
await petShop.addPetEffects();
}
this.close();
await pathmuncher.postImportCheck();
}
}
;// CONCATENATED MODULE: ./src/hooks/api.js
function registerAPI() {
game.modules.get(constants.MODULE_NAME).api = {
Pathmuncher: Pathmuncher,
PathmuncherImporter: PathmuncherImporter,
PetShop: PetShop,
CompendiumMatcher: CompendiumMatcher,
Seasoning: Seasoning,
CompendiumSelector: CompendiumSelector,
data: {
generateFeatMap: FEAT_RENAME_MAP,
equipment: EQUIPMENT_RENAME_MAP,
restrictedEquipment: RESTRICTED_EQUIPMENT,
feats: FEAT_RENAME_MAP(),
},
utils: src_utils,
CONSTANTS: constants,
};
}
;// CONCATENATED MODULE: ./src/hooks/settings.js
async function resetSettings() {
for (const [name, data] of Object.entries(constants.GET_DEFAULT_SETTINGS())) {
// eslint-disable-next-line no-await-in-loop
await game.settings.set(constants.MODULE_NAME, name, data.default);
}
window.location.reload();
}
class ResetSettingsDialog extends FormApplication {
constructor(...args) {
super(...args);
// eslint-disable-next-line no-constructor-return
return new Dialog({
title: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Title`),
content: `<p class="${constants.FLAG_NAME}-dialog-important">${game.i18n.localize(
`${constants.FLAG_NAME}.Dialogs.ResetSettings.Content`
)}</p>`,
buttons: {
confirm: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.ResetSettings.Confirm`),
callback: () => {
resetSettings();
},
},
cancel: {
icon: '<i class="fas fa-times"></i>',
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 = $(`<a class="pathmuncher-open" title="${constants.MODULE_FULL_NAME}"><i class="fas fa-hat-wizard"></i> ${constants.MODULE_FULL_NAME}</a>`);
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