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.
 
 
 

4312 lines
159 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"],
ancestryFeatures: ["pf2e.ancestryfeatures"],
classFeatures: ["pf2e.classfeatures"],
actions: ["pf2e.actionspf2e"],
spells: ["pf2e-psychic-amps.psychic-psi-cantrips", "pf2e.spells-srd"],
classes: ["pf2e.classes",],
ancestries: ["pf2e.ancestries",],
heritages: ["pf2e.heritages"],
equipment: ["pf2e.equipment-srd"],
formulas: ["pf2e.equipment-srd"],
deities: ["pf2e.deities"],
backgrounds: ["pf2e.backgrounds"],
},
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"
],
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",
],
actions: ["pf2e.actionspf2e"],
spells: ["pf2e-psychic-amps.psychic-psi-cantrips", "pf2e.spells-srd"],
classes: ["clerics.clerics-features", "pf2e.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: true,
},
// debug
[CONSTANTS.SETTINGS.LOG_LEVEL]: {
name: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Name`,
hint: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Hint`,
scope: "world",
config: true,
type: String,
choices: {
DEBUG: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.debug`,
INFO: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.info`,
WARN: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.warn`,
ERR: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.error`,
OFF: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.off`,
},
default: "WARN",
}
};
CONSTANTS.PATH = `modules/${CONSTANTS.MODULE_NAME}`;
/* harmony default export */ const constants = (CONSTANTS);
;// CONCATENATED MODULE: ./src/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;
}
},
};
/* 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) (.*)/,
];
// this equipment is named differently in foundry vs pathbuilder
const EQUIPMENT_RENAME_STATIC_MAP = [
{ pbName: "Chain", foundryName: "Chain (10 feet)" },
{ pbName: "Oil", foundryName: "Oil (1 pint)" },
{ pbName: "Bracelets of Dashing", foundryName: "Bracelet of Dashing" },
{ pbName: "Fingerprinting Kit", foundryName: "Fingerprint Kit" },
{ pbName: "Ladder", foundryName: "Ladder (10-foot)" },
{ pbName: "Mezmerizing Opal", foundryName: "Mesmerizing Opal" },
{ pbName: "Explorer's Clothing", foundryName: "Clothing (Explorer's)" },
{ pbName: "Flaming Star (Greater)", foundryName: "Greater Flaming Star" },
{ pbName: "Potion of Lesser Darkvision", foundryName: "Darkvision Elixir (Lesser)" },
{ pbName: "Potion of Greater Darkvision", foundryName: "Darkvision Elixir (Greater)" },
{ pbName: "Potion of Moderate Darkvision", foundryName: "Darkvision Elixir (Moderate)" },
{ pbName: "Bottled Sunlight", foundryName: "Formulated Sunlight" },
{ pbName: "Magazine (Repeating Hand Crossbow)", foundryName: "Magazine with 5 Bolts" },
{ pbName: "Astrolabe (Standard)", foundryName: "Standard Astrolabe" },
{ pbName: "Skinitch Salve", foundryName: "Skinstitch Salve" },
{ pbName: "Flawless Scale", foundryName: "Abadar's Flawless Scale" },
{ pbName: "Construct Key", foundryName: "Cordelia's Construct Key" },
{ pbName: "Construct Key (Greater)", foundryName: "Cordelia's Greater Construct Key" },
{ pbName: "Lesser Swapping Stone", foundryName: "Lesser Bonmuan Swapping Stone" },
{ pbName: "Major Swapping Stone", foundryName: "Major Bonmuan Swapping Stone" },
{ pbName: "Moderate Swapping Stone", foundryName: "Moderate Bonmuan Swapping Stone" },
{ pbName: "Greater Swapping Stone", foundryName: "Greater Bonmuan Swapping Stone" },
{ pbName: "Heartstone", foundryName: "Skarja's Heartstone" },
{ pbName: "Bullets (10 rounds)", foundryName: "Sling Bullets" },
{ pbName: "Hide", foundryName: "Hide Armor" },
{ pbName: "Soverign Glue", foundryName: "Sovereign Glue" },
];
function generateDynamicNames(pbName) {
const result = [];
// if we have a hardcoded map, don't return here
for (const reg of SWAPS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] });
}
}
return result;
}
function EQUIPMENT_RENAME_MAP(pbName = null) {
const postfixNames = pbName ? generateDynamicNames(pbName) : [];
return postfixNames.concat(EQUIPMENT_RENAME_STATIC_MAP);
}
// this is equipment is special and shouldn't have the transformations applied to it
const RESTRICTED_EQUIPMENT = [
"Bracers of Armor",
];
const IGNORED_EQUIPMENT = [
"Unarmored"
];
;// CONCATENATED MODULE: ./src/data/features.js
// these are features which are named differently in pathbuilder to foundry
const POSTFIX_PB_REMOVALS = [
/(.*) (Racket)$/,
/(.*) (Style)$/,
/(.*) (Initiate Benefit)$/,
// Cleric +
/(.*) (Doctrine)$/,
/(.*) (Element)$/,
/(.*) (Impulse Junction)$/,
/(.*) (Gate Junction:).*$/,
];
const PREFIX_PB_REMOVALS = [
/^(Arcane Thesis): (.*)/,
/^(Arcane School): (.*)/,
/^(The) (.*)/,
// Cleric +
/^(Blessing): (.*)/,
];
const POSTFIX_PB_SPLIT_AND_KEEP = [
/(.*) (Impulse Junction)$/,
/(.*) Gate Junction: (.*)$/,
];
const PARENTHESIS = [
/^(.*) \((.*)\)$/,
];
const SPLITS = [
/^(.*): (.*)/,
];
const features_SWAPS = [
/^(Greater) (.*)/,
/^(Lesser) (.*)/,
/^(Major) (.*)/,
];
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: "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" },
];
function features_generateDynamicNames(pbName) {
const result = [];
// if we have a hardcoded map, don't return here
if (FEAT_RENAME_STATIC_MAP.some((e) => e.pbName === pbName)) return result;
for (const reg of POSTFIX_PB_REMOVALS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: match[1], details: match[2] });
}
}
for (const reg of PREFIX_PB_REMOVALS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: match[2], details: match[1] });
}
}
for (const reg of SPLITS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: match[2], details: match[1] });
}
}
for (const reg of PARENTHESIS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: match[1], details: match[2] });
}
}
for (const reg of features_SWAPS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] });
}
}
return result;
}
function FEAT_RENAME_MAP(pbName = null) {
const postfixNames = pbName ? features_generateDynamicNames(pbName) : [];
return postfixNames.concat(FEAT_RENAME_STATIC_MAP);
}
const SHARED_IGNORE_LIST = [
"Draconic Rage", // just handled by effects on Draconic Instinct
"Mirror Initiate Benefit",
"Spellstrike Specifics",
"Unarmored",
"Simple Weapon Expertise",
"Spellbook",
"Energy Emanation", // pathbuilder does not pass through a type for this
"Imprecise Sense", // this gets picked up and added by granted features
"Imprecise Scent", // this gets picked up and added by granted features
];
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 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;
}
;// 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: "Scorching Ray", foundryName: "Blazing Bolt" },
{ pbName: "Burning Hands", foundryName: "Breathe Fire" },
{ pbName: "Calm Emotions", foundryName: "Calm" },
{ pbName: "Comprehend Languages", foundryName: "Translate" },
{ pbName: "Purify Food and Drink", foundryName: "Cleanse Cuisine" },
{ pbName: "Entangle", foundryName: "Entangling Flora" },
{ pbName: "Endure Elements", foundryName: "Environmental Endurance" },
{ pbName: "Meteor Swarm", foundryName: "Falling Stars" },
{ pbName: "Plane Shift", foundryName: "Interplanar Teleport" },
{ pbName: "Know Direction", foundryName: "Know the Way" },
{ pbName: "Stoneskin", foundryName: "Mountain Resilience" },
{ pbName: "Mage Armor", foundryName: "Mystic Armor" },
{ pbName: "Tree Stride", foundryName: "Nature's Pathway" },
{ pbName: "Barkskin", foundryName: "Oaken Resilience" },
{ pbName: "Tree Shape", foundryName: "One with Plants" },
{ pbName: "Meld into Stone", foundryName: "One with Stone" },
{ pbName: "Gentle Repose", foundryName: "Peaceful Rest" },
{ pbName: "Flesh to Stone", foundryName: "Petrify" },
{ pbName: "Dimensional Lock", foundryName: "Planar Seal" },
{ pbName: "Magic Fang", foundryName: "Runic Body" },
{ pbName: "Magic Weapon", foundryName: "Runic Weapon" },
{ pbName: "See Invisibility", foundryName: "See the Unseen" },
{ pbName: "Longstrider", foundryName: "Tailwind" },
{ pbName: "Tanglefoot", foundryName: "Tangle Vine" },
{ pbName: "Mage Hand", foundryName: "Telekinetic Hand" },
{ pbName: "Dimension Door", foundryName: "Translocate" },
{ pbName: "Tongues", foundryName: "Truespeech" },
{ pbName: "Gaseous Form", foundryName: "Vapor Form" },
];
function spellRename(spellName) {
if (foundry.utils.isNewerVersion(game.system.version, "5.3.0")) {
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) {
return game.pf2e.system.sluggify(name);
}
// sluggify with dromedary casing
static slugD(name) {
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 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) {
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.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 ancestryParagonVariant = game.settings.get("pf2e", "ancestryParagonVariant");
const dualClassVariant = game.settings.get("pf2e", "dualClassVariant");
// 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, location, feature });
// eslint-disable-next-line max-depth
if (ancestryParagonVariant && feature.type === "Ancestry Feat") {
document.system.location = "ancestry-bonus";
this.usedLocationsAlternateRules.add(location);
} else if (dualClassVariant && feature.type === "Class Feat") {
document.system.location = `dualclass-${feature.level}`;
this.usedLocationsAlternateRules.add(location);
}
}
}
}
#processSpecialData(name) {
if (name.includes("Domain: ")) {
const domainName = name.split(" ")[1];
this.parsed.feats.push({ name: "Deity's Domain", extra: domainName });
return true;
} else {
return false;
}
}
#getContainerData(key) {
return {
id: key,
containerName: this.source.equipmentContainers[key].containerName,
bagOfHolding: this.source.equipmentContainers[key].bagOfHolding,
backpack: this.source.equipmentContainers[key].backpack,
};
}
#nameMap() {
src_logger.debug("Starting Equipment Rename");
this.source.equipment
.filter((e) => e[0] && e[0] !== "undefined")
.forEach((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 = {
pbName: name,
qty: e[1],
added: false,
addedId: null,
addedAutoId: null,
inContainer: e[2] !== "Invested" ? e[2] : null,
container,
foundryId,
invested: e[2] === "Invested",
};
this.parsed.equipment.push(item);
});
this.source.armor
.filter((e) => e && e !== "undefined")
.forEach((e) => {
const name = Seasoning.getFoundryEquipmentName(e.name);
const item = mergeObject({
pbName: name,
originalName: e.name,
added: false,
addedId: null,
addedAutoId: null,
}, e);
this.parsed.armor.push(item);
});
this.source.weapons
.filter((e) => e && e !== "undefined")
.forEach((e) => {
const name = Seasoning.getFoundryEquipmentName(e.name);
const item = mergeObject({
pbName: name,
originalName:
e.name,
added: false,
addedId: null,
addedAutoId: null,
}, e);
this.parsed.weapons.push(item);
});
src_logger.debug("Finished Equipment Rename");
let i = 0;
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 name = this.getFoundryFeatureName(special).foundryName;
if (!this.#processSpecialData(name) && !Seasoning.IGNORED_SPECIALS().includes(name)) {
this.parsed.specials.push({ name, originalName: special, added: false, addedId: null, addedAutoId: null, rank: i });
i++;
}
});
src_logger.debug("Finished Special Rename");
let y = 0;
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: y,
};
this.parsed.feats.push(data);
y++;
});
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,
};
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,
});
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);
}
async #addDualClass(klass) {
if (!game.settings.get("pf2e", "dualClassVariant")) {
if (this.source.dualClass && this.source.dualClass !== "") {
src_logger.warn(`Imported character is dual class but system is not configured for dual class`, {
class: this.source.class,
dualClass: this.source.dualClass,
});
ui.notifications.warn(`Imported character is dual class but system is not configured for dual class`);
}
return;
}
if (!this.source.dualClass || this.source.dualClass === "") {
src_logger.warn(`Imported character not dual class but system is configured for dual class`, {
class: this.source.class,
});
ui.notifications.warn(`Imported character not dual class but system is configured for dual class`);
return;
}
// find the dual class
const 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();
src_logger.debug(`Dual Class ${dualClass.name} found, squashing things together.`);
klass.name = `${klass.name} - ${dualClass.name}`;
const ruleEntry = {
domain: "all",
key: "RollOption",
option: `class:${dualClass.system.slug}`,
};
// Attacks
["advanced", "martial", "simple", "unarmed"].forEach((key) => {
if (dualClass.system.attacks[key] > klass.system.attacks[key]) {
klass.system.attacks[key] = dualClass.system.attacks[key];
}
});
if (klass.system.attacks.martial <= dualClass.system.attacks.other.rank) {
if (dualClass.system.attacks.other.rank === klass.system.attacks.other.rank) {
let mashed = `${klass.system.attacks.other.name}, ${dualClass.system.attacks.other.name}`;
mashed = mashed.replace("and ", "");
klass.system.attacks.other.name = [...new Set(mashed.split(","))].join(",");
}
if (dualClass.system.attacks.other.rank > klass.system.attacks.other.rank) {
klass.system.attacks.other.name = dualClass.system.attacks.other.name;
klass.system.attacks.other.rank = dualClass.system.attacks.other.rank;
}
}
if (
klass.system.attacks.martial >= dualClass.system.attacks.other.rank
&& klass.system.attacks.martial >= klass.system.attacks.other.rank
) {
klass.system.attacks.other.rank = 0;
klass.system.attacks.other.name = "";
}
// Class DC
if (dualClass.system.classDC > klass.system.classDC) {
klass.system.classDC = dualClass.system.classDC;
}
// Defenses
["heavy", "light", "medium", "unarmored"].forEach((key) => {
if (dualClass.system.defenses[key] > klass.system.defenses[key]) {
klass.system.defenses[key] = dualClass.system.defenses[key];
}
});
// Description
klass.system.description.value = `${klass.system.description.value} ${dualClass.system.description.value}`;
// HP
if (dualClass.system.hp > klass.system.hp) {
klass.system.hp = dualClass.system.hp;
}
// Items
Object.entries(dualClass.system.items).forEach((i) => {
if (Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level > i[1].level)) {
Object.values(klass.system.items).find((x) => x.uuid === i[1].uuid).level = i[1].level;
} else if (!Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level <= i[1].level)) {
klass.system.items[i[0]] = i[1];
}
});
// Key Ability
dualClass.system.keyAbility.value.forEach((v) => {
if (!klass.system.keyAbility.value.includes(v)) {
klass.system.keyAbility.value.push(v);
}
});
// Perception
if (dualClass.system.perception > klass.system.perception) klass.system.perception = dualClass.system.perception;
// Rules
klass.system.rules.push(ruleEntry);
dualClass.system.rules.forEach((r) => {
if (!klass.system.rules.includes(r)) {
klass.system.rules.push(r);
}
});
klass.system.rules.forEach((r, i) => {
if (r.path !== undefined) {
const check = r.path.split(".");
if (
check.includes("data")
&& check.includes("martial")
&& check.includes("rank")
&& klass.system.attacks.martial >= r.value
) {
klass.system.rules.splice(i, 1);
}
}
});
// Saving Throws
["fortitude", "reflex", "will"].forEach((key) => {
if (dualClass.system.savingThrows[key] > klass.system.savingThrows[key]) {
klass.system.savingThrows[key] = dualClass.system.savingThrows[key];
}
});
// Skill Feat Levels
dualClass.system.skillFeatLevels.value.forEach((v) => {
klass.system.skillFeatLevels.value.push(v);
});
klass.system.skillFeatLevels.value = [...new Set(klass.system.skillFeatLevels.value)].sort((a, b) => {
return a - b;
});
// Skill Increase Levels
dualClass.system.skillIncreaseLevels.value.forEach((v) => {
klass.system.skillIncreaseLevels.value.push(v);
});
klass.system.skillIncreaseLevels.value = [...new Set(klass.system.skillIncreaseLevels.value)].sort((a, b) => {
return a - b;
});
// Trained Skills
if (dualClass.system.trainedSkills.additional > klass.system.trainedSkills.additional) {
klass.system.trainedSkills.additional = dualClass.system.trainedSkills.additional;
}
dualClass.system.trainedSkills.value.forEach((v) => {
if (!klass.system.trainedSkills.value.includes(v)) {
klass.system.trainedSkills.value.push(v);
}
});
this.result.dualClass = dualClass;
}
// eslint-disable-next-line class-methods-use-this
async #processGenericCompendiumLookup(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"
// }
#parsedFeatureMatch(type, document, slug, { ignoreAdded, isChoiceMatch = false, featType = null } = {}) {
// 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
&& (slug === Seasoning.slug(f.name)
|| 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))
|| (game.settings.get("pf2e", "dualClassVariant")
&& (slug
=== Seasoning.slug(Seasoning.getDualClassAdjustedSpecialNameLowerCase(f.name, this.source.dualClass))
|| slug
=== Seasoning.slug(
Seasoning.getDualClassAdjustedSpecialNameLowerCase(f.originalName, this.source.dualClass)
))))
);
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 } = {}) {
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, { originType = null, applyFeatLocation = false } = {}) {
src_logger.debug(`Adding granted item flags to ${document.name} (parent ${parent.name}) with originType "${originType}", and will applyFeatLocation? ${applyFeatLocation}`);
const camelCase = Seasoning.slugD(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 });
if (hasProperty(featureMatch, "added")) {
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,
});
}
}
if (matches.length > 0) {
if (choiceHint) {
const hintMatch = matches.find((m) => m.slug === Seasoning.slug(choiceHint));
if (hintMatch) return hintMatch;
}
const match = Pathmuncher.#getLowestChoiceRank(matches);
const featMatch = this.#findAllFeatureMatch(document, match.slug, { ignoreAdded });
if (adjustName && hasProperty(featMatch, "added")) {
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 });
if (featMatch) {
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);
}
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 = isNewerVersion(game.version, 11)
? new game.pf2e.RuleElements.all.ChoiceSet(cleansedChoiceSet, { parent: item })
: new game.pf2e.RuleElements.all.ChoiceSet(cleansedChoiceSet, item);
const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat();
const choices = isNewerVersion(game.version, 11)
? await choiceSetRules.inflateChoices(rollOptions, [])
: (await choiceSetRules.inflateChoices()).filter((c) => c.predicate?.test(rollOptions) ?? true);
src_logger.debug("Starting choice evaluation", {
document,
choiceSet,
item,
choiceSetRules,
rollOptions,
choices,
});
if (cleansedChoiceSet.choices?.query) {
const nonFilteredChoices = isNewerVersion(game.version, 11)
? await choiceSetRules.inflateChoices(rollOptions, [item])
: await choiceSetRules.inflateChoices();
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] });
// 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 = isNewerVersion(game.version, 11)
? new game.pf2e.RuleElements.all.GrantItem(cleansedRuleEntry, { parent: item })
: new game.pf2e.RuleElements.all.GrantItem(cleansedRuleEntry, 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"
? isNewerVersion(game.version, 11)
? new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, { parent: item })
: new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, item)
: isNewerVersion(game.version, 11)
? new game.pf2e.RuleElements.all.GrantItem(cleansedRule, { parent: item })
: new game.pf2e.RuleElements.all.GrantItem(cleansedRule, 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"
? isNewerVersion(game.version, 11)
? await ruleElement.inflateChoices(rollOptions, [item])
: (await ruleElement.inflateChoices()).filter((c) => !c.predicate || c.predicate.test(rollOptions))
: [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"
? isNewerVersion(game.version, 11)
? new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, { parent: item })
: new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, item)
: isNewerVersion(game.version, 11)
? new game.pf2e.RuleElements.all.GrantItem(cleansedRule, { parent: item })
: new game.pf2e.RuleElements.all.GrantItem(cleansedRule, 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;
}
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;
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);
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, { 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 boostLocation = foundry.utils.isNewerVersion(game.system.version, "5.3.0")
? "attributes"
: "abilities";
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.${boostLocation}.boosts`, classBoostMap);
this.boosts.class = classBoostMap;
// ancestry
} else {
this.boosts.custom = true;
if (foundry.utils.isNewerVersion("5.3.0", game.system.version)) {
["str", "dex", "con", "int", "wis", "cha"].forEach((key) => {
setProperty(this.result.character, `system.abilities.${key}.value`, this.source.abilities[key]);
});
} else {
["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;
}
#setSkills() {
setProperty(this.result.character, "system.skills.acr.rank", this.source.proficiencies.acrobatics / 2);
setProperty(this.result.character, "system.skills.arc.rank", this.source.proficiencies.arcana / 2);
setProperty(this.result.character, "system.skills.ath.rank", this.source.proficiencies.athletics / 2);
setProperty(this.result.character, "system.skills.cra.rank", this.source.proficiencies.crafting / 2);
setProperty(this.result.character, "system.skills.dec.rank", this.source.proficiencies.deception / 2);
setProperty(this.result.character, "system.skills.dip.rank", this.source.proficiencies.diplomacy / 2);
setProperty(this.result.character, "system.skills.itm.rank", this.source.proficiencies.intimidation / 2);
setProperty(this.result.character, "system.skills.med.rank", this.source.proficiencies.medicine / 2);
setProperty(this.result.character, "system.skills.nat.rank", this.source.proficiencies.nature / 2);
setProperty(this.result.character, "system.skills.occ.rank", this.source.proficiencies.occultism / 2);
setProperty(this.result.character, "system.skills.prf.rank", this.source.proficiencies.performance / 2);
setProperty(this.result.character, "system.skills.rel.rank", this.source.proficiencies.religion / 2);
setProperty(this.result.character, "system.skills.soc.rank", this.source.proficiencies.society / 2);
setProperty(this.result.character, "system.skills.ste.rank", this.source.proficiencies.stealth / 2);
setProperty(this.result.character, "system.skills.sur.rank", this.source.proficiencies.survival / 2);
setProperty(this.result.character, "system.skills.thi.rank", this.source.proficiencies.thievery / 2);
}
#setSaves() {
setProperty(this.result.character, "system.saves.fortitude.tank", this.source.proficiencies.fortitude / 2);
setProperty(this.result.character, "system.saves.reflex.value", this.source.proficiencies.reflex / 2);
setProperty(this.result.character, "system.saves.will.value", this.source.proficiencies.will / 2);
}
#setMartials() {
setProperty(this.result.character, "system.martial.advanced.rank", this.source.proficiencies.advanced / 2);
setProperty(this.result.character, "system.martial.heavy.rank", this.source.proficiencies.heavy / 2);
setProperty(this.result.character, "system.martial.light.rank", this.source.proficiencies.light / 2);
setProperty(this.result.character, "system.martial.medium.rank", this.source.proficiencies.medium / 2);
setProperty(this.result.character, "system.martial.unarmored.rank", this.source.proficiencies.unarmored / 2);
setProperty(this.result.character, "system.martial.martial.rank", this.source.proficiencies.martial / 2);
setProperty(this.result.character, "system.martial.simple.rank", this.source.proficiencies.simple / 2);
setProperty(this.result.character, "system.martial.unarmed.rank", this.source.proficiencies.unarmed / 2);
}
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))
|| (game.settings.get("pf2e", "dualClassVariant")
&& 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);
}
});
}
async #generateFeatItems(type, { levelCap = null, parsedFilter = null } = {}) {
src_logger.debug(`Generate feat items for ${type} with level cap "${levelCap}" and filter "${parsedFilter}"`);
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 (parsedFilter && pBFeat.type !== parsedFilter) continue;
src_logger.debug("Generating feature for", pBFeat);
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: parsedFilter,
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({
pbName: 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.pbName);
if (!indexMatch) {
src_logger.error(`Unable to match ${e.pbName}`, e);
this.bad.push({ pbName: e.pbName, type: "equipment", details: { e } });
continue;
}
const doc = await 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 applyRunes(parsedItem, itemData, type) {
itemData.system.potencyRune.value = parsedItem.pot;
if (type === "weapon") {
itemData.system.strikingRune.value = parsedItem.str;
} else if (type === "armor") {
itemData.system.resiliencyRune.value = parsedItem.res;
}
if (type === "armor" && parsedItem.worn
&& ((Number.isInteger(parsedItem.pot) && parsedItem.pot > 0)
|| (parsedItem.res && parsedItem.res !== "")
)
) {
itemData.system.equipped.invested = true;
}
if (parsedItem.runes[0]) itemData.system.propertyRune1.value = Seasoning.slugD(parsedItem.runes[0]);
if (parsedItem.runes[1]) itemData.system.propertyRune2.value = Seasoning.slugD(parsedItem.runes[1]);
if (parsedItem.runes[2]) itemData.system.propertyRune3.value = Seasoning.slugD(parsedItem.runes[2]);
if (parsedItem.runes[3]) itemData.system.propertyRune4.value = Seasoning.slugD(parsedItem.runes[3]);
if (parsedItem.mat) {
const material = parsedItem.mat.split(" (")[0];
itemData.system.preciousMaterial.value = Seasoning.slugD(material);
itemData.system.preciousMaterialGrade.value = Seasoning.getMaterialGrade(parsedItem.mat);
}
}
async #generateWeaponItems() {
for (const w of this.parsed.weapons) {
if (Seasoning.IGNORED_EQUIPMENT().includes(w.pbName)) {
w.added = true;
w.addedAutoId = "ignored";
continue;
}
src_logger.debug("Generating weapon for", w);
const indexMatch = this.compendiumMatchers["equipment"].getMatch(w.pbName, w.pbName);
if (!indexMatch) {
src_logger.error(`Unable to match weapon item ${w.name}`, w);
this.bad.push({ pbName: w.pbName, type: "weapon", details: { w } });
continue;
}
const doc = await indexMatch.pack.getDocument(indexMatch.i._id);
const itemData = doc.toObject();
itemData._id = foundry.utils.randomID();
itemData.system.quantity = w.qty;
// because some shields don't have damage dice, but come in as weapons on pathbuilder
if (itemData.type === "weapon") {
itemData.system.damage.die = w.die;
Pathmuncher.applyRunes(w, itemData, "weapon");
}
if (w.display) itemData.name = w.display;
this.#resizeItem(itemData);
this.result.weapons.push(itemData);
w.added = true;
w.addedId = itemData._id;
}
}
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);
const indexMatch = this.compendiumMatchers["equipment"].getMatch(`${a.pbName} Armor`, a.pbName);
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 = doc.toObject();
itemData._id = foundry.utils.randomID();
itemData.system.equipped.value = a.worn ?? false;
if (!Seasoning.RESTRICTED_EQUIPMENT().some((i) => itemData.name.startsWith(i))) {
itemData.system.equipped.inSlot = a.worn ?? false;
itemData.system.quantity = a.qty;
itemData.system.category = a.prof;
const isShield = itemData.system.category === "shield";
itemData.system.equipped.handsHeld = isShield && a.worn ? 1 : 0;
itemData.system.equipped.carryType = isShield && a.worn ? "held" : "worn";
Pathmuncher.applyRunes(a, itemData, "armor");
}
if (a.display) itemData.name = a.display;
this.#resizeItem(itemData);
this.result.armor.push(itemData);
// eslint-disable-next-line require-atomic-updates
a.added = true;
a.addedId = itemData._id;
}
}
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.category.value"],
});
await ritualCompendium.loadCompendiums();
const ritualFilters = {
"system.category.value": "ritual",
};
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, ritualFilters);
if (!indexMatch) {
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
await this.#generateFeatItems("feats", { parsedFilter: "Ancestry Feat" });
await this.#generateFeatItems("feats", { parsedFilter: "Skill Feat" });
await this.#generateFeatItems("feats", { parsedFilter: "Class Feat" });
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);
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();
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.actor.updateEmbeddedDocuments("Item", this.result.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) {
["abilities"].forEach((location) => {
const abilityTargets = ["str", "dex", "con", "int", "wis", "cha"]
.filter((ability) => hasProperty(this.actor, `system.${location}.${ability}`));
const abilityDeletions = abilityTargets
.reduce(
(accumulated, ability) => ({
...accumulated,
[`-=${ability}`]: null,
}),
{}
);
setProperty(this.result.character, `system.${location}`, abilityDeletions);
});
}
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").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.name}</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