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.
 
 
 

2813 lines
106 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",
},
GET_DEFAULT_SETTINGS() {
return foundry.utils.deepClone(CONSTANTS.DEFAULT_SETTINGS);
},
};
CONSTANTS.DEFAULT_SETTINGS = {
// Enable options
[CONSTANTS.SETTINGS.RESTRICT_TO_TRUSTED]: {
name: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Name`,
hint: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Hint`,
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: debouncedReload,
},
[CONSTANTS.SETTINGS.ADD_VISION_FEATS]: {
name: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Name`,
hint: `${CONSTANTS.FLAG_NAME}.Settings.AddVisionFeats.Hint`,
scope: "player",
config: true,
type: Boolean,
default: true,
},
// debug
[CONSTANTS.SETTINGS.LOG_LEVEL]: {
name: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Name`,
hint: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.Hint`,
scope: "world",
config: true,
type: String,
choices: {
DEBUG: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.debug`,
INFO: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.info`,
WARN: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.warn`,
ERR: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.error`,
OFF: `${CONSTANTS.FLAG_NAME}.Settings.LogLevel.off`,
},
default: "WARN",
}
};
CONSTANTS.PATH = `modules/${CONSTANTS.MODULE_NAME}`;
/* harmony default export */ const constants = (CONSTANTS);
;// CONCATENATED MODULE: ./src/data/equipment.js
const SWAPS = [
/^(Greater) (.*)/,
/^(Lesser) (.*)/,
/^(Major) (.*)/,
/^(Moderate) (.*)/,
/^(Standard) (.*)/,
];
// this equipment is named differently in foundry vs pathbuilder
const EQUIPMENT_RENAME_STATIC_MAP = [
{ pbName: "Chain", foundryName: "Chain (10 feet)" },
{ pbName: "Oil", foundryName: "Oil (1 pint)" },
{ pbName: "Bracelets of Dashing", foundryName: "Bracelet of Dashing" },
{ pbName: "Fingerprinting Kit", foundryName: "Fingerprint Kit" },
{ pbName: "Ladder", foundryName: "Ladder (10-foot)" },
{ pbName: "Mezmerizing Opal", foundryName: "Mesmerizing Opal" },
{ pbName: "Explorer's Clothing", foundryName: "Clothing (Explorer's)" },
{ pbName: "Flaming Star (Greater)", foundryName: "Greater Flaming Star" },
{ pbName: "Potion of Lesser Darkvision", foundryName: "Darkvision Elixir (Lesser)" },
{ pbName: "Potion of Greater Darkvision", foundryName: "Darkvision Elixir (Greater)" },
{ pbName: "Potion of Moderate Darkvision", foundryName: "Darkvision Elixir (Moderate)" },
{ pbName: "Bottled Sunlight", foundryName: "Formulated Sunlight" },
{ pbName: "Magazine (Repeating Hand Crossbow)", foundryName: "Magazine with 5 Bolts" },
{ pbName: "Astrolabe (Standard)", foundryName: "Standard Astrolabe" },
{ pbName: "Skinitch Salve", foundryName: "Skinstitch Salve" },
{ pbName: "Flawless Scale", foundryName: "Abadar's Flawless Scale" },
{ pbName: "Construct Key", foundryName: "Cordelia's Construct Key" },
{ pbName: "Construct Key (Greater)", foundryName: "Cordelia's Greater Construct Key" },
{ pbName: "Lesser Swapping Stone", foundryName: "Lesser Bonmuan Swapping Stone" },
{ pbName: "Major Swapping Stone", foundryName: "Major Bonmuan Swapping Stone" },
{ pbName: "Moderate Swapping Stone", foundryName: "Moderate Bonmuan Swapping Stone" },
{ pbName: "Greater Swapping Stone", foundryName: "Greater Bonmuan Swapping Stone" },
{ pbName: "Heartstone", foundryName: "Skarja's Heartstone" },
{ pbName: "Bullets (10 rounds)", foundryName: "Sling Bullets" },
];
function generateDynamicNames(pbName) {
const result = [];
// if we have a hardcoded map, don't return here
for (const reg of SWAPS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] });
}
}
return result;
}
function EQUIPMENT_RENAME_MAP(pbName = null) {
const postfixNames = pbName ? generateDynamicNames(pbName) : [];
return postfixNames.concat(EQUIPMENT_RENAME_STATIC_MAP);
}
// this is equipment is special and shouldn't have the transformations applied to it
const RESTRICTED_EQUIPMENT = [
"Bracers of Armor",
];
const IGNORED_EQUIPMENT = [
"Unarmored"
];
;// CONCATENATED MODULE: ./src/utils.js
const utils = {
wait: async (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
},
capitalize: (s) => {
if (typeof s !== "string") return "";
return s.charAt(0).toUpperCase() + s.slice(1);
},
setting: (key) => {
return game.settings.get(constants.MODULE_NAME, key);
},
updateSetting: async (key, value) => {
return game.settings.set(constants.MODULE_NAME, key, value);
},
getFlags: (actor) => {
const flags = actor.flags[constants.FLAG_NAME]
? actor.flags[constants.FLAG_NAME]
: {
pathbuilderId: undefined,
addFeats: true,
addEquipment: true,
addBackground: true,
addHeritage: true,
addAncestry: true,
addSpells: true,
addMoney: true,
addTreasure: true,
addLores: true,
addWeapons: true,
addArmor: true,
addDeity: true,
addName: true,
addClass: true,
addFamiliars: true,
addFormulas: true,
askForChoices: false,
};
return flags;
},
setFlags: async (actor, flags) => {
let updateData = {};
setProperty(updateData, `flags.${constants.FLAG_NAME}`, flags);
await actor.update(updateData);
return actor;
},
resetFlags: async (actor) => {
return utils.setFlags(actor, null);
},
getOrCreateFolder: async (root, entityType, folderName, folderColor = "") => {
let folder = game.folders.contents.find((f) =>
f.type === entityType && f.name === folderName
// if a root folder we want to match the root id for the parent folder
&& (root ? root.id : null) === (f.folder?.id ?? null)
);
// console.warn(`Looking for ${root} ${entityType} ${folderName}`);
// console.warn(folder);
if (folder) return folder;
folder = await Folder.create(
{
name: folderName,
type: entityType,
color: folderColor,
parent: (root) ? root.id : null,
},
{ displaySheet: false }
);
return folder;
},
// eslint-disable-next-line no-unused-vars
getFolder: async (kind, subFolder = "", baseFolderName = "Pathmuncher", baseColor = "#6f0006", subColor = "#98020a", typeFolder = true) => {
let entityTypes = new Map();
entityTypes.set("pets", "Pets");
const folderName = game.i18n.localize(`${constants.MODULE_NAME}.labels.${kind}`);
const entityType = entityTypes.get(kind);
const baseFolder = await utils.getOrCreateFolder(null, entityType, baseFolderName, baseColor);
const entityFolder = typeFolder ? await utils.getOrCreateFolder(baseFolder, entityType, folderName, subColor) : baseFolder;
if (subFolder !== "") {
const subFolderName = subFolder.charAt(0).toUpperCase() + subFolder.slice(1);
const typeFolder = await utils.getOrCreateFolder(entityFolder, entityType, subFolderName, subColor);
return typeFolder;
} else {
return entityFolder;
}
},
};
/* harmony default export */ const src_utils = (utils);
;// CONCATENATED MODULE: ./src/data/features.js
// these are features which are named differently in pathbuilder to foundry
const POSTFIX_PB_REMOVALS = [
/(.*) (Racket)$/,
/(.*) (Style)$/,
];
const PREFIX_PB_REMOVALS = [
/^(Arcane Thesis): (.*)/,
/^(Arcane School): (.*)/,
/^(The) (.*)/,
];
const PARENTHESIS = [
/^(.*) \((.*)\)$/,
];
const SPLITS = [
/^(.*): (.*)/,
];
const features_SWAPS = [
/^(Greater) (.*)/,
/^(Lesser) (.*)/,
/^(Major) (.*)/,
];
const FEAT_RENAME_STATIC_MAP = [
{ pbName: "Aerialist", foundryName: "Shory Aerialist" },
{ pbName: "Aeromancer", foundryName: "Shory Aeromancer" },
{ pbName: "Ancient-Blooded", foundryName: "Ancient-Blooded Dwarf" },
{ pbName: "Antipaladin [Chaotic Evil]", foundryName: "Antipaladin" },
{ pbName: "Ape", foundryName: "Ape Animal Instinct" },
{ pbName: "Aquatic Eyes (Darkvision)", foundryName: "Aquatic Eyes" },
{ pbName: "Astrology", foundryName: "Saoc Astrology" },
{ pbName: "Battle Ready", foundryName: "Battle-Ready Orc" },
{ pbName: "Bite (Gnoll)", foundryName: "Bite" },
{ pbName: "Bloodline: Genie (Efreeti)", foundryName: "Bloodline: Genie" },
{ pbName: "Bloody Debilitations", foundryName: "Bloody Debilitation" },
{ pbName: "Cave Climber Kobold", foundryName: "Caveclimber Kobold" },
{ pbName: "Chosen One", foundryName: "Chosen of Lamashtu" },
{ pbName: "Cognative Mutagen (Greater)", foundryName: "Cognitive Mutagen (Greater)" },
{ pbName: "Cognative Mutagen (Lesser)", foundryName: "Cognitive Mutagen (Lesser)" },
{ pbName: "Cognative Mutagen (Major)", foundryName: "Cognitive Mutagen (Major)" },
{ pbName: "Cognative Mutagen (Moderate)", foundryName: "Cognitive Mutagen (Moderate)" },
{ pbName: "Cognitive Crossover", foundryName: "Kreighton's Cognitive Crossover" },
{ pbName: "Collegiate Attendant Dedication", foundryName: "Magaambyan Attendant Dedication" },
{ pbName: "Construct Carver", foundryName: "Tupilaq Carver" },
{ pbName: "Constructed (Android)", foundryName: "Constructed" },
{ pbName: "Deadly Hair", foundryName: "Syu Tak-nwa's Deadly Hair" },
{ pbName: "Deepvision", foundryName: "Deep Vision" },
{ pbName: "Deflect Arrows", foundryName: "Deflect Arrow" },
{ pbName: "Desecrator [Neutral Evil]", foundryName: "Desecrator" },
{ pbName: "Detective Dedication", foundryName: "Edgewatch Detective Dedication" },
{ pbName: "Duelist Dedication (LO)", foundryName: "Aldori Duelist Dedication" },
{ pbName: "Dwarven Hold Education", foundryName: "Dongun Education" },
{ pbName: "Ember's Eyes (Darkvision)", foundryName: "Ember's Eyes" },
{ pbName: "Enhanced Familiar Feat", foundryName: "Enhanced Familiar" },
{ pbName: "Enigma", foundryName: "Enigma Muse" },
{ pbName: "Escape", foundryName: "Fane's Escape" },
{ pbName: "Eye of the Arcane Lords", foundryName: "Eye of the Arclords" },
{ pbName: "Flip", foundryName: "Farabellus Flip" },
{ pbName: "Fourberie", foundryName: "Fane's Fourberie" },
{ pbName: "Ganzi Gaze (Low-Light Vision)", foundryName: "Ganzi Gaze" },
{ pbName: "Guild Agent Dedication", foundryName: "Pathfinder Agent Dedication" },
{ pbName: "Harmful Font", foundryName: "Divine Font" },
{ pbName: "Healing Font", foundryName: "Divine Font" },
{ pbName: "Heatwave", foundryName: "Heat Wave" },
{ pbName: "Heavenseeker Dedication", foundryName: "Jalmeri Heavenseeker Dedication" },
{ pbName: "Heir of the Astrologers", foundryName: "Heir of the Saoc" },
{ pbName: "High Killer Training", foundryName: "Vernai Training" },
{ pbName: "Ice-Witch", foundryName: "Irriseni Ice-Witch" },
{ pbName: "Impeccable Crafter", foundryName: "Impeccable Crafting" },
{ pbName: "Incredible Beastmaster's Companion", foundryName: "Incredible Beastmaster Companion" },
{ pbName: "Interrogation", foundryName: "Bolera's Interrogation" },
{ pbName: "Katana", foundryName: "Katana Weapon Familiarity" },
{ pbName: "Liberator [Chaotic Good]", foundryName: "Liberator" },
{ pbName: "Lumberjack Dedication", foundryName: "Turpin Rowe Lumberjack Dedication" },
{ pbName: "Maestro", foundryName: "Maestro Muse" },
{ pbName: "Major Lesson I", foundryName: "Major Lesson" },
{ pbName: "Major Lesson II", foundryName: "Major Lesson" },
{ pbName: "Major Lesson III", foundryName: "Major Lesson" },
{ pbName: "Mantis God's Grip", foundryName: "Achaekek's Grip" },
{ pbName: "Marked for Death", foundryName: "Mark for Death" },
{ pbName: "Miraculous Spells", foundryName: "Miraculous Spell" },
{ pbName: "Multifarious", foundryName: "Multifarious Muse" },
{ pbName: "Paladin [Lawful Good]", foundryName: "Paladin" },
{ pbName: "Parry", foundryName: "Aldori Parry" },
{ pbName: "Polymath", foundryName: "Polymath Muse" },
{ pbName: "Precise Debilitation", foundryName: "Precise Debilitations" },
{ pbName: "Quick Climber", foundryName: "Quick Climb" },
{ pbName: "Recognise Threat", foundryName: "Recognize Threat" },
{ pbName: "Redeemer [Neutral Good]", foundryName: "Redeemer" },
{ pbName: "Revivification Protocall", foundryName: "Revivification Protocol" },
{ pbName: "Riposte", foundryName: "Aldori Riposte" },
{ pbName: "Rkoan Arts", foundryName: "Rokoan Arts" },
{ pbName: "Saberteeth", foundryName: "Saber Teeth" },
{ pbName: "Scholarly Recollection", foundryName: "Uzunjati Recollection" },
{ pbName: "Scholarly Storytelling", foundryName: "Uzunjati Storytelling" },
{ pbName: "Secret Lesson", foundryName: "Janatimo's Lessons" },
{ pbName: "Sentry Dedication", foundryName: "Lastwall Sentry Dedication" },
{ pbName: "Stab and Snag", foundryName: "Stella's Stab and Snag" },
{ pbName: "Tenets of Evil", foundryName: "The Tenets of Evil" },
{ pbName: "Tenets of Good", foundryName: "The Tenets of Good" },
{ pbName: "Tongue of the Sun and Moon", foundryName: "Tongue of Sun and Moon" },
{ pbName: "Tribal Bond", foundryName: "Quah Bond" },
{ pbName: "Tyrant [Lawful Evil]", foundryName: "Tyrant" },
{ pbName: "Vestigal Wings", foundryName: "Vestigial Wings" },
{ pbName: "Virtue-Forged Tattooed", foundryName: "Virtue-Forged Tattoos" },
{ pbName: "Wakizashi", foundryName: "Wakizashi Weapon Familiarity" },
{ pbName: "Warden", foundryName: "Lastwall Warden" },
{ pbName: "Warrior", foundryName: "Warrior Muse" },
{ pbName: "Wary Eye", foundryName: "Eye of Ozem" },
{ pbName: "Wayfinder Resonance Infiltrator", foundryName: "Westyr's Wayfinder Repository" },
{ pbName: "Wind God's Fan", foundryName: "Wind God’s Fan" },
{ pbName: "Wind God’s Fan", foundryName: "Wind God's Fan" },
{ pbName: "Black", foundryName: "Black Dragon" },
{ pbName: "Brine", foundryName: "Brine Dragon" },
{ pbName: "Copper", foundryName: "Copper Dragon" },
{ pbName: "Blue", foundryName: "Blue Dragon" },
{ pbName: "Bronze", foundryName: "Bronze Dragon" },
{ pbName: "Cloud", foundryName: "Cloud Dragon" },
{ pbName: "Sky", foundryName: "Sky Dragon" },
{ pbName: "Brass", foundryName: "Brass Dragon" },
{ pbName: "Underworld", foundryName: "Underworld Dragon" },
{ pbName: "Crystal", foundryName: "Crystal Dragon" },
{ pbName: "Forest", foundryName: "Forest Dragon" },
{ pbName: "Green", foundryName: "Green Dragon" },
{ pbName: "Sea", foundryName: "Sea Dragon" },
{ pbName: "Silver", foundryName: "Silver Dragon" },
{ pbName: "White", foundryName: "White Dragon" },
{ pbName: "Sovereign", foundryName: "Sovereign Dragon" },
{ pbName: "Umbral", foundryName: "Umbral Dragon" },
{ pbName: "Red", foundryName: "Red Dragon" },
{ pbName: "Gold", foundryName: "Gold Dragon" },
{ pbName: "Magma", foundryName: "Magma Dragon" },
];
function features_generateDynamicNames(pbName) {
const result = [];
// if we have a hardcoded map, don't return here
if (FEAT_RENAME_STATIC_MAP.some((e) => e.pbName === pbName)) return result;
for (const reg of POSTFIX_PB_REMOVALS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: match[1], details: match[2] });
}
}
for (const reg of PREFIX_PB_REMOVALS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: match[2], details: match[1] });
}
}
for (const reg of SPLITS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: match[2], details: match[1] });
}
}
for (const reg of PARENTHESIS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: match[1], details: match[2] });
}
}
for (const reg of features_SWAPS) {
const match = pbName.match(reg);
if (match) {
result.push({ pbName, foundryName: `${match[2]} (${match[1]})`, details: match[2] });
}
}
return result;
}
function FEAT_RENAME_MAP(pbName = null) {
const postfixNames = pbName ? features_generateDynamicNames(pbName) : [];
return postfixNames.concat(FEAT_RENAME_STATIC_MAP);
}
const IGNORED_FEATS_LIST = [
"Unarmored",
"Simple Weapon Expertise",
"Spellbook",
"Energy Emanation", // pathbuilder does not pass through a type for this
"Imprecise Sense", // this gets picked up and added by granted features
];
function IGNORED_FEATS() {
const visionFeats = src_utils.setting(constants.SETTINGS.ADD_VISION_FEATS) ? [] : ["Low-Light Vision", "Darkvision"];
return IGNORED_FEATS_LIST.concat(visionFeats);
}
;// CONCATENATED MODULE: ./src/logger.js
const logger = {
_showMessage: (logLevel, data) => {
if (!logLevel || !data || typeof logLevel !== "string") {
return false;
}
const setting = src_utils.setting(constants.SETTINGS.LOG_LEVEL);
const logLevels = ["DEBUG", "INFO", "WARN", "ERR", "OFF"];
const logLevelIndex = logLevels.indexOf(logLevel.toUpperCase());
if (setting == "OFF" || logLevelIndex === -1 || logLevelIndex < logLevels.indexOf(setting)) {
return false;
}
return true;
},
log: (logLevel, ...data) => {
if (!logger._showMessage(logLevel, data)) {
return;
}
logLevel = logLevel.toUpperCase();
let msg = "No logging message provided. Please see the payload for more information.";
let payload = data.slice();
if (data[0] && typeof (data[0] == "string")) {
msg = data[0];
if (data.length > 1) {
payload = data.slice(1);
} else {
payload = null;
}
}
msg = `${constants.MODULE_NAME} | ${logLevel} > ${msg}`;
switch (logLevel) {
case "DEBUG":
if (payload) {
console.debug(msg, ...payload); // eslint-disable-line no-console
} else {
console.debug(msg); // eslint-disable-line no-console
}
break;
case "INFO":
if (payload) {
console.info(msg, ...payload); // eslint-disable-line no-console
} else {
console.info(msg); // eslint-disable-line no-console
}
break;
case "WARN":
if (payload) {
console.warn(msg, ...payload); // eslint-disable-line no-console
} else {
console.warn(msg); // eslint-disable-line no-console
}
break;
case "ERR":
if (payload) {
console.error(msg, ...payload); // eslint-disable-line no-console
} else {
console.error(msg); // eslint-disable-line no-console
}
break;
default:
break;
}
},
debug: (...data) => {
logger.log("DEBUG", ...data);
},
info: (...data) => {
logger.log("INFO", ...data);
},
warn: (...data) => {
logger.log("WARN", ...data);
},
error: (...data) => {
logger.log("ERR", ...data);
},
};
/* harmony default export */ const src_logger = (logger);
;// CONCATENATED MODULE: ./src/app/Pathmuncher.js
/* eslint-disable no-await-in-loop */
/* eslint-disable no-continue */
class Pathmuncher {
// eslint-disable-next-line class-methods-use-this
EQUIPMENT_RENAME_MAP(name) {
return EQUIPMENT_RENAME_MAP(name);
}
getFoundryEquipmentName(pbName) {
return this.EQUIPMENT_RENAME_MAP(pbName).find((map) => map.pbName == pbName)?.foundryName ?? pbName;
}
FEAT_RENAME_MAP(name) {
const dynamicItems = [
{ pbName: "Shining Oath", foundryName: `Shining Oath (${this.getChampionType()})` },
{ pbName: "Counterspell", foundryName: `Counterspell (${src_utils.capitalize(this.getClassSpellCastingType() ?? "")})` },
{ pbName: "Counterspell", foundryName: `Counterspell (${src_utils.capitalize(this.getClassSpellCastingType(true) ?? "")})` },
{ pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${this.source.class})` },
{ pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${this.source.dualClass})` },
{ pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${src_utils.capitalize(this.getClassSpellCastingType() ?? "")} Caster)` },
{ pbName: "Cantrip Expansion", foundryName: `Cantrip Expansion (${src_utils.capitalize(this.getClassSpellCastingType(true) ?? "")} Caster)` },
];
return FEAT_RENAME_MAP(name).concat(dynamicItems);
}
getFoundryFeatureName(pbName) {
const match = this.FEAT_RENAME_MAP(pbName).find((map) => map.pbName == pbName);
return match ?? { pbName, foundryName: pbName, details: undefined };
}
// eslint-disable-next-line class-methods-use-this
get RESTRICTED_EQUIPMENT() {
return RESTRICTED_EQUIPMENT;
}
// specials that are handled by Foundry and shouldn't be added
// eslint-disable-next-line class-methods-use-this
get IGNORED_FEATURES() {
return IGNORED_FEATS();
};
// eslint-disable-next-line class-methods-use-this
get IGNORED_EQUIPMENT() {
return IGNORED_EQUIPMENT;
};
getChampionType() {
if (this.source.alignment == "LG") return "Paladin";
else if (this.source.alignment == "CG") return "Liberator";
else if (this.source.alignment == "NG") return "Redeemer";
else if (this.source.alignment == "LE") return "Tyrant";
else if (this.source.alignment == "CE") return "Antipaladin";
else if (this.source.alignment == "NE") return "Desecrator";
return "Unknown";
}
constructor(actor, { addFeats = true, addEquipment = true, addSpells = true, addMoney = true, addLores = true,
addWeapons = true, addArmor = true, addTreasure = true, addDeity = true, addName = true, addClass = true,
addBackground = true, addHeritage = true, addAncestry = true, askForChoices = false } = {}
) {
this.actor = actor;
// note not all these options do anything yet!
this.options = {
addTreasure,
addMoney,
addFeats,
addSpells,
addEquipment,
addLores,
addWeapons,
addArmor,
addDeity,
addName,
addClass,
addBackground,
addHeritage,
addAncestry,
askForChoices,
};
this.source = null;
this.parsed = {
specials: [],
feats: [],
equipment: [],
armor: [],
weapons: [],
};
this.usedLocations = new Set();
this.usedLocationsAlternateRules = new Set();
this.autoAddedFeatureIds = new Set();
this.autoAddedFeatureItems = {};
this.allFeatureRules = {};
this.autoAddedFeatureRules = {};
this.grantItemLookUp = {};
this.autoFeats = [];
this.result = {
character: {
_id: this.actor.id,
prototypeToken: {},
},
class: [],
deity: [],
heritage: [],
ancestry: [],
background: [],
casters: [],
spells: [],
feats: [],
weapons: [],
armor: [],
equipment: [],
lores: [],
money: [],
treasure: [],
adventurersPack: {
item: null,
contents: [
{ slug: "bedroll", qty: 1 },
{ slug: "chalk", qty: 10 },
{ slug: "flint-and-steel", qty: 1 },
{ slug: "rope", qty: 1 },
{ slug: "rations", qty: 14 },
{ slug: "torch", qty: 5 },
{ slug: "waterskin", qty: 1 },
],
},
focusPool: 0,
};
this.check = {};
this.bad = [];
}
async fetchPathbuilder(pathbuilderId) {
if (!pathbuilderId) {
const flags = src_utils.getFlags(this.actor);
pathbuilderId = flags?.pathbuilderId;
}
if (pathbuilderId) {
const jsonData = await foundry.utils.fetchJsonWithTimeout(`https://www.pathbuilder2e.com/json.php?id=${pathbuilderId}`);
if (jsonData.success) {
this.source = jsonData.build;
} else {
ui.notifications.warn(game.i18n.format(`${constants.FLAG_NAME}.Dialogs.Pathmuncher.FetchFailed`, { pathbuilderId }));
}
} else {
ui.notifications.error(game.i18n.localize(`${constants.FLAG_NAME}.Dialogs.Pathmuncher.NoId`));
}
}
getClassAdjustedSpecialNameLowerCase(name) {
return `${name} (${this.source.class})`.toLowerCase();
}
getDualClassAdjustedSpecialNameLowerCase(name) {
return `${name} (${this.source.dualClass})`.toLowerCase();
}
getAncestryAdjustedSpecialNameLowerCase(name) {
return `${name} (${this.source.ancestry})`.toLowerCase();
}
getHeritageAdjustedSpecialNameLowerCase(name) {
return `${name} (${this.source.heritage})`.toLowerCase();
}
static getMaterialGrade(material) {
if (material.toLowerCase().includes("high-grade")) {
return "high";
} else if (material.toLowerCase().includes("standard-grade")) {
return "standard";
}
return "low";
}
static getFoundryFeatLocation(pathbuilderFeatType, pathbuilderFeatLevel) {
if (pathbuilderFeatType === "Ancestry Feat") {
return `ancestry-${pathbuilderFeatLevel}`;
} else if (pathbuilderFeatType === "Class Feat") {
return `class-${pathbuilderFeatLevel}`;
} else if (pathbuilderFeatType === "Skill Feat") {
return `skill-${pathbuilderFeatLevel}`;
} else if (pathbuilderFeatType === "General Feat") {
return `general-${pathbuilderFeatLevel}`;
} else if (pathbuilderFeatType === "Background Feat") {
return `skill-${pathbuilderFeatLevel}`;
} else if (pathbuilderFeatType === "Archetype Feat") {
return `archetype-${pathbuilderFeatLevel}`;
} else {
return null;
}
}
#generateFoundryFeatLocation(document, feature) {
if (feature.type && feature.level) {
const ancestryParagonVariant = game.settings.get("pf2e", "ancestryParagonVariant");
const dualClassVariant = game.settings.get("pf2e", "dualClassVariant");
// const freeArchetypeVariant = game.settings.get("pf2e", "freeArchetypeVariant");
const location = Pathmuncher.getFoundryFeatLocation(feature.type, feature.level);
if (location && !this.usedLocations.has(location)) {
document.system.location = location;
this.usedLocations.add(location);
} else if (location && this.usedLocations.has(location)) {
src_logger.debug("Variant feat location", { ancestryParagonVariant, location, feature });
// eslint-disable-next-line max-depth
if (ancestryParagonVariant && feature.type === "Ancestry Feat") {
document.system.location = "ancestry-bonus";
this.usedLocationsAlternateRules.add(location);
} else if (dualClassVariant && feature.type === "Class Feat") {
document.system.location = `dualclass-${feature.level}`;
this.usedLocationsAlternateRules.add(location);
}
}
}
}
#processSpecialData(name) {
if (name.includes("Domain: ")) {
const domainName = name.split(" ")[1];
this.parsed.feats.push({ name: "Deity's Domain", extra: domainName });
return true;
} else {
return false;
}
}
#nameMap() {
src_logger.debug("Starting Equipment Rename");
this.source.equipment
.filter((e) => e[0] && e[0] !== "undefined")
.forEach((e) => {
const name = this.getFoundryEquipmentName(e[0]);
const item = { pbName: name, qty: e[1], added: false };
this.parsed.equipment.push(item);
});
this.source.armor
.filter((e) => e && e !== "undefined")
.forEach((e) => {
const name = this.getFoundryEquipmentName(e.name);
const item = mergeObject({ pbName: name, originalName: e.name, added: false }, e);
this.parsed.armor.push(item);
});
this.source.weapons
.filter((e) => e && e !== "undefined")
.forEach((e) => {
const name = this.getFoundryEquipmentName(e.name);
const item = mergeObject({ pbName: name, originalName: e.name, added: false }, e);
this.parsed.weapons.push(item);
});
src_logger.debug("Finished Equipment Rename");
src_logger.debug("Starting Special Rename");
this.source.specials
.filter((special) => special
&& special !== "undefined"
&& special !== "Not Selected"
&& special !== this.source.heritage
)
.forEach((special) => {
const name = this.getFoundryFeatureName(special).foundryName;
if (!this.#processSpecialData(name) && !this.IGNORED_FEATURES.includes(name)) {
this.parsed.specials.push({ name, originalName: special, added: false });
}
});
src_logger.debug("Finished Special Rename");
src_logger.debug("Starting Feat Rename");
this.source.feats
.filter((feat) => feat[0]
&& feat[0] !== "undefined"
&& feat[0] !== "Not Selected"
&& feat[0] !== this.source.heritage
)
.forEach((feat) => {
const name = this.getFoundryFeatureName(feat[0]).foundryName;
const data = {
name,
extra: feat[1],
added: false,
type: feat[2],
level: feat[3],
originalName: feat[0],
};
this.parsed.feats.push(data);
});
src_logger.debug("Finished Feat Rename");
}
#prepare() {
this.#nameMap();
}
static getSizeValue(size) {
switch (size) {
case 0:
return "tiny";
case 1:
return "sm";
case 3:
return "lg";
default:
return "med";
}
}
async #processSenses() {
const senses = [];
this.source.specials.forEach((special) => {
if (special === "Low-Light Vision") {
senses.push({ type: "lowLightVision" });
} else if (special === "Darkvision") {
senses.push({ type: "darkvision" });
} else if (special === "Scent") {
senses.push({ type: "scent" });
}
});
setProperty(this.result.character, "system.traits.senses", senses);
}
async #addDualClass(klass) {
if (!game.settings.get("pf2e", "dualClassVariant")) {
if (this.source.dualClass && this.source.dualClass !== "") {
src_logger.warn(`Imported character is dual class but system is not configured for dual class`, {
class: this.source.class,
dualClass: this.source.dualClass,
});
ui.notifications.warn(`Imported character is dual class but system is not configured for dual class`);
}
return;
}
if (!this.source.dualClass || this.source.dualClass === "") {
src_logger.warn(`Imported character not dual class but system is configured for dual class`, {
class: this.source.class,
});
ui.notifications.warn(`Imported character not dual class but system is configured for dual class`);
return;
}
// find the dual class
const compendium = await game.packs.get("pf2e.classes");
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
const foundryName = this.getFoundryFeatureName(this.source.dualClass).foundryName;
const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(foundryName))
?? index.find((i) => i.system.slug === game.pf2e.system.sluggify(this.source.dualClass));
if (!indexMatch) return;
const doc = await compendium.getDocument(indexMatch._id);
const dualClass = doc.toObject();
src_logger.debug(`Dual Class ${dualClass.name} found, squashing things together.`);
klass.name = `${klass.name} - ${dualClass.name}`;
const ruleEntry = {
"domain": "all",
"key": "RollOption",
"option": `class:${dualClass.system.slug}`
};
// Attacks
["advanced", "martial", "simple", "unarmed"].forEach((key) => {
if (dualClass.system.attacks[key] > klass.system.attacks[key]) {
klass.system.attacks[key] = dualClass.system.attacks[key];
}
});
if (klass.system.attacks.martial <= dualClass.system.attacks.other.rank) {
if (dualClass.system.attacks.other.rank === klass.system.attacks.other.rank) {
let mashed = `${klass.system.attacks.other.name}, ${dualClass.system.attacks.other.name}`;
mashed = mashed.replace("and ", "");
klass.system.attacks.other.name = [...new Set(mashed.split(','))].join(',');
}
if (dualClass.system.attacks.other.rank > klass.system.attacks.other.rank) {
klass.system.attacks.other.name = dualClass.system.attacks.other.name;
klass.system.attacks.other.rank = dualClass.system.attacks.other.rank;
}
}
if (klass.system.attacks.martial >= dualClass.system.attacks.other.rank
&& klass.system.attacks.martial >= klass.system.attacks.other.rank
) {
klass.system.attacks.other.rank = 0;
klass.system.attacks.other.name = "";
}
// Class DC
if (dualClass.system.classDC > klass.system.classDC) {
klass.system.classDC = dualClass.system.classDC;
}
// Defenses
["heavy", "light", "medium", "unarmored"].forEach((key) => {
if (dualClass.system.defenses[key] > klass.system.defenses[key]) {
klass.system.defenses[key] = dualClass.system.defenses[key];
}
});
// Description
klass.system.description.value = `${klass.system.description.value} ${dualClass.system.description.value}`;
// HP
if (dualClass.system.hp > klass.system.hp) {
klass.system.hp = dualClass.system.hp;
}
// Items
Object.entries(dualClass.system.items).forEach((i) => {
if (Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level > i[1].level)) {
Object.values(klass.system.items).find((x) => x.uuid === i[1].uuid).level = i[1].level;
} else if (!Object.values(klass.system.items).some((x) => x.uuid === i[1].uuid && x.level <= i[1].level)) {
klass.system.items[i[0]] = i[1];
}
});
// Key Ability
dualClass.system.keyAbility.value.forEach((v) => {
if (!klass.system.keyAbility.value.includes(v)) {
klass.system.keyAbility.value.push(v);
}
});
// Perception
if (dualClass.system.perception > klass.system.perception) klass.system.perception = dualClass.system.perception;
// Rules
klass.system.rules.push(ruleEntry);
dualClass.system.rules.forEach((r) => {
if (!klass.system.rules.includes(r)) {
klass.system.rules.push(r);
}
});
klass.system.rules.forEach((r, i) => {
if (r.path !== undefined) {
const check = r.path.split('.');
if (check.includes("data")
&& check.includes("martial")
&& check.includes("rank")
&& klass.system.attacks.martial >= r.value
) {
klass.system.rules.splice(i, 1);
}
}
});
// Saving Throws
["fortitude", "reflex", "will"].forEach((key) => {
if (dualClass.system.savingThrows[key] > klass.system.savingThrows[key]) {
klass.system.savingThrows[key] = dualClass.system.savingThrows[key];
}
});
// Skill Feat Levels
dualClass.system.skillFeatLevels.value.forEach((v) => {
klass.system.skillFeatLevels.value.push(v);
});
klass.system.skillFeatLevels.value = [...new Set(klass.system.skillFeatLevels.value)].sort((a, b) => {
return a - b;
});
// Skill Increase Levels
dualClass.system.skillIncreaseLevels.value.forEach((v) => {
klass.system.skillIncreaseLevels.value.push(v);
});
klass.system.skillIncreaseLevels.value = [...new Set(klass.system.skillIncreaseLevels.value)].sort((a, b) => {
return a - b;
});
// Trained Skills
if (dualClass.system.trainedSkills.additional > klass.system.trainedSkills.additional) {
klass.system.trainedSkills.additional = dualClass.system.trainedSkills.additional;
}
dualClass.system.trainedSkills.value.forEach((v) => {
if (!klass.system.trainedSkills.value.includes(v)) {
klass.system.trainedSkills.value.push(v);
}
});
this.result.dualClass = dualClass;
}
// eslint-disable-next-line class-methods-use-this
async #processGenericCompendiumLookup(compendiumLabel, name, target) {
src_logger.debug(`Checking for compendium documents for ${name} (${target}) in ${compendiumLabel}`);
const compendium = await game.packs.get(compendiumLabel);
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
const foundryName = this.getFoundryFeatureName(name).foundryName;
const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(foundryName))
?? index.find((i) => i.system.slug === game.pf2e.system.sluggify(name));
if (indexMatch) {
const doc = await compendium.getDocument(indexMatch._id);
const itemData = doc.toObject();
if (target === "class") {
itemData.system.keyAbility.selected = this.source.keyability;
await this.#addDualClass(itemData);
}
itemData._id = foundry.utils.randomID();
this.#generateGrantItemData(itemData);
this.result[target].push(itemData);
await this.#addGrantedItems(itemData);
return true;
} else {
this.bad.push({ pbName: name, type: target, details: { name } });
return false;
}
}
// for grants, e.g. ont he champion "Deity and Cause" where there are choices.
// how do we determine and match these? should we?
// "pf2e": {
// "itemGrants": {
// "adanye": {
// "id": "4GHcp3iaREfj2ZgN",
// "onDelete": "detach"
// },
// "paladin": {
// "id": "HGWkTEatliHgDaEu",
// "onDelete": "detach"
// }
// }
// }
// "Paladin" (granted by deity and casue)
// "pf2e": {
// "grantedBy": {
// "id": "xnrkrJa2YE1UOAVy",
// "onDelete": "cascade"
// },
// "itemGrants": {
// "retributiveStrike": {
// "id": "WVHbj9LljCTovdsv",
// "onDelete": "detach"
// }
// }
// }
// retributive strike
// "pf2e": {
// "grantedBy": {
// "id": "HGWkTEatliHgDaEu",
// "onDelete": "cascade"
// }
#parsedFeatureMatch(type, slug, ignoreAdded) {
// console.warn(`Trying to find ${slug} in ${type}, ignoreAdded? ${ignoreAdded}`);
const parsedMatch = this.parsed[type].find((f) =>
(!ignoreAdded || (ignoreAdded && !f.added))
&& (
slug === game.pf2e.system.sluggify(f.name)
|| slug === game.pf2e.system.sluggify(this.getClassAdjustedSpecialNameLowerCase(f.name))
|| slug === game.pf2e.system.sluggify(this.getAncestryAdjustedSpecialNameLowerCase(f.name))
|| slug === game.pf2e.system.sluggify(this.getHeritageAdjustedSpecialNameLowerCase(f.name))
|| slug === game.pf2e.system.sluggify(f.originalName)
|| slug === game.pf2e.system.sluggify(this.getClassAdjustedSpecialNameLowerCase(f.originalName))
|| slug === game.pf2e.system.sluggify(this.getAncestryAdjustedSpecialNameLowerCase(f.originalName))
|| slug === game.pf2e.system.sluggify(this.getHeritageAdjustedSpecialNameLowerCase(f.originalName))
|| (game.settings.get("pf2e", "dualClassVariant")
&& (slug === game.pf2e.system.sluggify(this.getDualClassAdjustedSpecialNameLowerCase(f.name))
|| slug === game.pf2e.system.sluggify(this.getDualClassAdjustedSpecialNameLowerCase(f.originalName))
)
)
)
);
// console.warn(`Results of find ${slug} in ${type}, ignoreAdded? ${ignoreAdded}`, {
// slug,
// parsedMatch,
// parsed: duplicate(this.parsed),
// });
return parsedMatch;
}
#generatedResultMatch(type, slug) {
const featMatch = this.result[type].find((f) => slug === f.system.slug);
return featMatch;
}
#findAllFeatureMatch(slug, ignoreAdded) {
const featMatch = this.#parsedFeatureMatch("feats", slug, ignoreAdded);
if (featMatch) return featMatch;
const specialMatch = this.#parsedFeatureMatch("specials", slug, ignoreAdded);
if (specialMatch) return specialMatch;
const deityMatch = this.#generatedResultMatch("deity", slug);
return deityMatch;
// const classMatch = this.#generatedResultMatch("class", slug);
// return classMatch;
// const equipmentMatch = this.#generatedResultMatch("equipment", slug);
// return equipmentMatch;
}
#createGrantedItem(document, parent) {
src_logger.debug(`Adding granted item flags to ${document.name} (parent ${parent.name})`);
const camelCase = game.pf2e.system.sluggify(document.system.slug, { camel: "dromedary" });
setProperty(parent, `flags.pf2e.itemGrants.${camelCase}`, { id: document._id, onDelete: "detach" });
setProperty(document, "flags.pf2e.grantedBy", { id: parent._id, onDelete: "cascade" });
this.autoFeats.push(document);
if (!this.options.askForChoices) {
this.result.feats.push(document);
}
const featureMatch = this.#findAllFeatureMatch(document.system.slug, true)
?? (document.name.includes("(")
? this.#findAllFeatureMatch(game.pf2e.system.sluggify(document.name.split("(")[0].trim()), true)
: undefined
);
// console.warn(`Matching feature for ${document.name}?`, {
// featureMatch,
// });
if (featureMatch) {
if (hasProperty(featureMatch, "added")) {
featureMatch.added = true;
this.#generateFoundryFeatLocation(document, featureMatch);
}
return;
}
if (document.type !== "action") src_logger.warn(`Unable to find parsed feature match for granted feature ${document.name}. This might not be an issue, but might indicate feature duplication.`, { document, parent });
}
async #featureChoiceMatch(choices, ignoreAdded, adjustName) {
for (const choice of choices) {
const doc = adjustName
? game.i18n.localize(choice.label)
: await fromUuid(choice.value);
if (!doc) continue;
const slug = adjustName
? game.pf2e.system.sluggify(doc)
: doc.system.slug;
const featMatch = this.#findAllFeatureMatch(slug, ignoreAdded);
if (featMatch) {
if (adjustName && hasProperty(featMatch, "added")) featMatch.added = true;
src_logger.debug("Choices evaluated", { choices, document, featMatch, choice });
return choice;
}
}
return undefined;
}
async #evaluateChoices(document, choiceSet) {
src_logger.debug(`Evaluating choices for ${document.name}`, { document, choiceSet });
const tempActor = await this.#generateTempActor();
const cleansedChoiceSet = deepClone(choiceSet);
try {
const item = tempActor.getEmbeddedDocument("Item", document._id);
const choiceSetRules = new game.pf2e.RuleElements.all.ChoiceSet(cleansedChoiceSet, item);
const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat();
const choices = (await choiceSetRules.inflateChoices()).filter((c) => !c.predicate || c.predicate.test(rollOptions));
src_logger.debug("Starting choice evaluation", {
document,
choiceSet,
item,
choiceSetRules,
rollOptions,
choices,
});
src_logger.debug("Evaluating choiceset", cleansedChoiceSet);
const choiceMatch = await this.#featureChoiceMatch(choices, true, cleansedChoiceSet.adjustName);
src_logger.debug("choiceMatch result", choiceMatch);
if (choiceMatch) return choiceMatch;
if (typeof cleansedChoiceSet.choices === "string" || Array.isArray(choices)) {
for (const choice of choices) {
const featMatch = this.#findAllFeatureMatch(choice.value, true, cleansedChoiceSet.adjustName);
if (featMatch) {
src_logger.debug("Choices evaluated", { cleansedChoiceSet, choices, document, featMatch, choice });
featMatch.added = true;
choice.nouuid = true;
return choice;
}
}
}
} catch (err) {
src_logger.error("Whoa! Something went major bad wrong during choice evaluation", {
err,
tempActor: tempActor.toObject(),
document: duplicate(document),
choiceSet: duplicate(cleansedChoiceSet),
});
throw err;
} finally {
await Actor.deleteDocuments([tempActor._id]);
}
src_logger.debug("Evaluate Choices failed", { choiceSet: cleansedChoiceSet, tempActor, document });
return undefined;
}
async #resolveInjectedUuid(source, propertyData) {
if (source === null || typeof source === "number" || (typeof source === "string" && !source.includes("{"))) {
return source;
}
// Walk the object tree and resolve any string values found
if (Array.isArray(source)) {
for (let i = 0; i < source.length; i++) {
source[i] = this.#resolveInjectedUuid(source[i]);
}
} else if (typeof source === 'object' && source !== null) {
for (const [key, value] of Object.entries(source)) {
if (typeof value === "string" || (typeof value === 'object' && value !== null)) {
source[key] = this.#resolveInjectedUuid(value);
}
}
return source;
} else if (typeof source === "string") {
const match = source.match(/{(actor|item|rule)\|(.*?)}/);
if (match && match[1] === "actor") {
return String(getProperty(this.result.character, match[1]));
} else if (match) {
const value = this.grantItemLookUp[match[0]].uuid;
if (!value) {
src_logger.error("Failed to resolve injected property", {
source,
propertyData,
key: match[1],
prop: match[2],
});
}
return String(value);
} else {
src_logger.error("Failed to resolve injected property", {
source,
propertyData,
});
}
}
return source;
}
async #generateGrantItemData(document) {
src_logger.debug(`Generating grantItem rule lookups for ${document.name}...`, { document: deepClone(document) });
for (const rule of document.system.rules.filter((r) => r.key === "GrantItem" && r.uuid.includes("{"))) {
src_logger.debug("Generating rule for...", { document: deepClone(document), rule });
const match = rule.uuid.match(/{(item|rule)\|(.*?)}/);
if (match) {
const flagName = match[2].split(".").pop();
const choiceSet = document.system.rules.find((rule) => rule.key === "ChoiceSet" && rule.flag === flagName)
?? document.system.rules.find((rule) => rule.key === "ChoiceSet");
const choice = choiceSet ? (await this.#evaluateChoices(document, choiceSet)) : undefined;
const value = choice?.value ?? undefined;
if (!value) {
src_logger.warn("Failed to resolve injected uuid", {
ruleData: choiceSet,
flagName,
key: match[1],
prop: match[2],
value,
});
} else {
src_logger.debug(`Generated lookup ${value} for key ${document.name}`);
}
this.grantItemLookUp[rule.uuid] = {
docId: document.id,
key: rule.uuid,
choice,
uuid: value,
flag: flagName,
choiceSet,
};
this.grantItemLookUp[`${document._id}-${flagName}`] = {
docId: document.id,
key: rule.uuid,
choice,
uuid: value,
flag: flagName,
choiceSet,
};
this.grantItemLookUp[`${document._id}`] = {
docId: document.id,
key: rule.uuid,
choice,
uuid: value,
flag: flagName,
choiceSet,
};
this.grantItemLookUp[`${document._id}-${flagName}`] = {
docId: document.id,
key: rule.uuid,
choice,
uuid: value,
flag: flagName,
choiceSet,
};
} else {
src_logger.error("Failed to resolve injected uuid", {
document,
rule,
});
}
}
}
async #checkRule(document, rule) {
const tempActor = await this.#generateTempActor([document]);
const cleansedRule = deepClone(rule);
try {
const item = tempActor.getEmbeddedDocument("Item", document._id);
const ruleElement = cleansedRule.key === "ChoiceSet"
? new game.pf2e.RuleElements.all.ChoiceSet(cleansedRule, item)
: new game.pf2e.RuleElements.all.GrantItem(cleansedRule, item);
const rollOptions = [tempActor.getRollOptions(), item.getRollOptions("item")].flat();
const choices = cleansedRule.key === "ChoiceSet"
? (await ruleElement.inflateChoices()).filter((c) => !c.predicate || c.predicate.test(rollOptions))
: [ruleElement.resolveValue()];
const isGood = cleansedRule.key === "ChoiceSet"
? (await this.#featureChoiceMatch(choices, false)) !== undefined
: ruleElement.test(rollOptions);
return isGood;
} catch (err) {
src_logger.error("Something has gone most wrong during rule checking", {
document,
rule: cleansedRule,
tempActor,
});
throw err;
} finally {
await Actor.deleteDocuments([tempActor._id]);
}
}
// eslint-disable-next-line complexity
async #addGrantedRules(document) {
if (document.system.rules.length === 0) return;
src_logger.debug(`addGrantedRules for ${document.name}`, duplicate(document));
if (hasProperty(document, "system.level.value")
&& document.system.level.value > this.result.character.system.details.level.value
) {
return;
}
// const rulesToKeep = document.system.rules.filter((r) => !["GrantItem", "ChoiceSet", "MartialProficiency"].includes(r.key));
const rulesToKeep = [];
this.allFeatureRules[document._id] = deepClone(document.system.rules);
this.autoAddedFeatureRules[document._id] = deepClone(document.system.rules.filter((r) => !["GrantItem", "ChoiceSet"].includes(r.key)));
await this.#generateGrantItemData(document);
const grantRules = document.system.rules.filter((r) => r.key === "GrantItem");
const choiceRules = document.system.rules.filter((r) => r.key === "ChoiceSet");
for (const ruleTypes of [choiceRules, grantRules]) {
for (const rawRuleEntry of ruleTypes) {
const ruleEntry = deepClone(rawRuleEntry);
src_logger.debug(`Checking ${document.name} rule key: ${ruleEntry.key}`);
const lookupName = ruleEntry.flag ? `${document._id}-${ruleEntry.flag}` : document._id;
src_logger.debug("Rule check, looking up", {
id: `${document._id}-${ruleEntry.flag}`,
lookup: this.grantItemLookUp[lookupName],
lookups: this.grantItemLookUp,
ruleEntry,
lookupName,
});
// have we pre-evaluated this choice?
const choice = ruleEntry.key === "ChoiceSet"
? this.grantItemLookUp[lookupName]?.choice
? this.grantItemLookUp[lookupName].choice
: await this.#evaluateChoices(document, ruleEntry)
: undefined;
const uuid = ruleEntry.key === "GrantItem"
? await this.#resolveInjectedUuid(ruleEntry.uuid, ruleEntry)
: choice?.value;
src_logger.debug(`UUID for ${document.name}: "${uuid}"`, document, ruleEntry, choice);
const ruleFeature = uuid ? await fromUuid(uuid) : undefined;
if (ruleFeature) {
const featureDoc = ruleFeature.toObject();
featureDoc._id = foundry.utils.randomID();
if (featureDoc.system.rules) this.allFeatureRules[document._id] = deepClone(document.system.rules);
setProperty(featureDoc, "flags.pathmuncher.origin.uuid", uuid);
if (this.autoAddedFeatureIds.has(`${ruleFeature.id}${ruleFeature.type}`)) {
src_logger.debug(`Feature ${featureDoc.name} found for ${document.name}, but has already been added (${ruleFeature.id})`, ruleFeature);
continue;
}
src_logger.debug(`Found rule feature ${featureDoc.name} for ${document.name} for`, ruleEntry);
if (ruleEntry.predicate) {
const testResult = await this.#checkRule(featureDoc, ruleEntry);
// eslint-disable-next-line max-depth
if (!testResult) {
const data = { document, ruleEntry, featureDoc, testResult };
src_logger.debug(`The test failed for ${document.name} rule key: ${ruleFeature.key} (This is probably not a problem).`, data);
continue;
}
}
if (choice) {
ruleEntry.selection = choice.value;
}
this.autoAddedFeatureIds.add(`${ruleFeature.id}${ruleFeature.type}`);
featureDoc._id = foundry.utils.randomID();
this.#createGrantedItem(featureDoc, document);
if (hasProperty(ruleFeature, "system.rules.length")) await this.#addGrantedRules(featureDoc);
} else if (choice?.nouuid) {
src_logger.debug("Parsed no id rule", { choice, uuid, ruleEntry });
if (!ruleEntry.flag) ruleEntry.flag = game.pf2e.system.sluggify(document.name, { camel: "dromedary" });
ruleEntry.selection = choice.value;
if (choice.label) document.name = `${document.name} (${choice.label})`;
} else if (choice && uuid && !hasProperty(ruleEntry, "selection")) {
src_logger.debug("Parsed odd choice rule", { choice, uuid, ruleEntry });
if (!ruleEntry.flag) ruleEntry.flag = game.pf2e.system.sluggify(document.name, { camel: "dromedary" });
ruleEntry.selection = choice.value;
if (ruleEntry.adjustName && choice.label) {
const label = game.i18n.localize(choice.label);
const name = `${document.name} (${label})`;
const pattern = (() => {
const escaped = RegExp.escape(label);
return new RegExp(`\\(${escaped}\\) \\(${escaped}\\)$`);
})();
document.name = name.replace(pattern, `(${label})`);
}
} else {
const data = {
uuid: ruleEntry.uuid,
document,
ruleEntry,
choice,
lookup: this.grantItemLookUp[ruleEntry.uuid],
};
if (ruleEntry.key === "GrantItem" && this.grantItemLookUp[ruleEntry.uuid]) {
rulesToKeep.push(ruleEntry);
// const lookup = this.grantItemLookUp[ruleEntry.uuid].choiceSet
// eslint-disable-next-line max-depth
// if (!rulesToKeep.some((r) => r.key == lookup && r.prompt === lookup.prompt)) {
// rulesToKeep.push(this.grantItemLookUp[ruleEntry.uuid].choiceSet);
// }
} else if (ruleEntry.key === "ChoiceSet" && !hasProperty(ruleEntry, "flag")) {
src_logger.debug("Prompting user for choices", ruleEntry);
rulesToKeep.push(ruleEntry);
}
src_logger.warn("Unable to determine granted rule feature, needs better parser", data);
}
this.autoAddedFeatureRules[document._id].push(ruleEntry);
}
}
if (!this.options.askForChoices) {
// eslint-disable-next-line require-atomic-updates
document.system.rules = rulesToKeep;
}
}
async #addGrantedItems(document) {
if (hasProperty(document, "system.items")) {
src_logger.debug(`addGrantedItems for ${document.name}`, duplicate(document));
if (!this.autoAddedFeatureItems[document._id]) {
this.autoAddedFeatureItems[document._id] = duplicate(document.system.items);
}
const failedFeatureItems = {};
for (const [key, grantedItemFeature] of Object.entries(document.system.items)) {
src_logger.debug(`Checking granted item ${document.name}, with key: ${key}`, grantedItemFeature);
if (grantedItemFeature.level > getProperty(this.result.character, "system.details.level.value")) continue;
const feature = await fromUuid(grantedItemFeature.uuid);
if (!feature) {
const data = { uuid: grantedItemFeature.uuid, grantedFeature: grantedItemFeature, feature };
src_logger.warn("Unable to determine granted item feature, needs better parser", data);
failedFeatureItems[key] = grantedItemFeature;
continue;
}
this.autoAddedFeatureIds.add(`${feature.id}${feature.type}`);
const featureDoc = feature.toObject();
featureDoc._id = foundry.utils.randomID();
setProperty(featureDoc.system, "location", document._id);
this.#createGrantedItem(featureDoc, document);
if (hasProperty(featureDoc, "system.rules")) await this.#addGrantedRules(featureDoc);
}
if (!this.options.askForChoices) {
// eslint-disable-next-line require-atomic-updates
document.system.items = failedFeatureItems;
}
}
if (hasProperty(document, "system.rules")) await this.#addGrantedRules(document);
}
async #detectGrantedFeatures() {
if (this.result.class.length > 0) await this.#addGrantedItems(this.result.class[0]);
if (this.result.ancestry.length > 0) await this.#addGrantedItems(this.result.ancestry[0]);
if (this.result.heritage.length > 0) await this.#addGrantedItems(this.result.heritage[0]);
if (this.result.background.length > 0) await this.#addGrantedItems(this.result.background[0]);
}
async #processCore() {
setProperty(this.result.character, "name", this.source.name);
setProperty(this.result.character, "prototypeToken.name", this.source.name);
setProperty(this.result.character, "system.details.level.value", this.source.level);
if (this.source.age !== "Not set") setProperty(this.result.character, "system.details.age.value", this.source.age);
if (this.source.gender !== "Not set") setProperty(this.result.character, "system.details.gender.value", this.source.gender);
setProperty(this.result.character, "system.details.alignment.value", this.source.alignment);
setProperty(this.result.character, "system.details.keyability.value", this.source.keyability);
if (this.source.deity !== "Not set") setProperty(this.result.character, "system.details.deity.value", this.source.deity);
setProperty(this.result.character, "system.traits.size.value", Pathmuncher.getSizeValue(this.source.size));
setProperty(this.result.character, "system.traits.languages.value", this.source.languages.map((l) => l.toLowerCase()));
this.#processSenses();
setProperty(this.result.character, "system.abilities.str.value", this.source.abilities.str);
setProperty(this.result.character, "system.abilities.dex.value", this.source.abilities.dex);
setProperty(this.result.character, "system.abilities.con.value", this.source.abilities.con);
setProperty(this.result.character, "system.abilities.int.value", this.source.abilities.int);
setProperty(this.result.character, "system.abilities.wis.value", this.source.abilities.wis);
setProperty(this.result.character, "system.abilities.cha.value", this.source.abilities.cha);
setProperty(this.result.character, "system.saves.fortitude.tank", this.source.proficiencies.fortitude / 2);
setProperty(this.result.character, "system.saves.reflex.value", this.source.proficiencies.reflex / 2);
setProperty(this.result.character, "system.saves.will.value", this.source.proficiencies.will / 2);
setProperty(this.result.character, "system.martial.advanced.rank", this.source.proficiencies.advanced / 2);
setProperty(this.result.character, "system.martial.heavy.rank", this.source.proficiencies.heavy / 2);
setProperty(this.result.character, "system.martial.light.rank", this.source.proficiencies.light / 2);
setProperty(this.result.character, "system.martial.medium.rank", this.source.proficiencies.medium / 2);
setProperty(this.result.character, "system.martial.unarmored.rank", this.source.proficiencies.unarmored / 2);
setProperty(this.result.character, "system.martial.martial.rank", this.source.proficiencies.martial / 2);
setProperty(this.result.character, "system.martial.simple.rank", this.source.proficiencies.simple / 2);
setProperty(this.result.character, "system.martial.unarmed.rank", this.source.proficiencies.unarmed / 2);
setProperty(this.result.character, "system.skills.acr.rank", this.source.proficiencies.acrobatics / 2);
setProperty(this.result.character, "system.skills.arc.rank", this.source.proficiencies.arcana / 2);
setProperty(this.result.character, "system.skills.ath.rank", this.source.proficiencies.athletics / 2);
setProperty(this.result.character, "system.skills.cra.rank", this.source.proficiencies.crafting / 2);
setProperty(this.result.character, "system.skills.dec.rank", this.source.proficiencies.deception / 2);
setProperty(this.result.character, "system.skills.dip.rank", this.source.proficiencies.diplomacy / 2);
setProperty(this.result.character, "system.skills.itm.rank", this.source.proficiencies.intimidation / 2);
setProperty(this.result.character, "system.skills.med.rank", this.source.proficiencies.medicine / 2);
setProperty(this.result.character, "system.skills.nat.rank", this.source.proficiencies.nature / 2);
setProperty(this.result.character, "system.skills.occ.rank", this.source.proficiencies.occultism / 2);
setProperty(this.result.character, "system.skills.prf.rank", this.source.proficiencies.performance / 2);
setProperty(this.result.character, "system.skills.rel.rank", this.source.proficiencies.religion / 2);
setProperty(this.result.character, "system.skills.soc.rank", this.source.proficiencies.society / 2);
setProperty(this.result.character, "system.skills.ste.rank", this.source.proficiencies.stealth / 2);
setProperty(this.result.character, "system.skills.sur.rank", this.source.proficiencies.survival / 2);
setProperty(this.result.character, "system.skills.thi.rank", this.source.proficiencies.thievery / 2);
setProperty(this.result.character, "system.attributes.perception.rank", this.source.proficiencies.perception / 2);
setProperty(this.result.character, "system.attributes.classDC.rank", this.source.proficiencies.classDC / 2);
}
#indexFind(index, arrayOfNameMatches) {
for (const name of arrayOfNameMatches) {
const indexMatch = index.find((i) =>
i.system.slug === game.pf2e.system.sluggify(name)
|| i.system.slug === game.pf2e.system.sluggify(this.getClassAdjustedSpecialNameLowerCase(name))
|| i.system.slug === game.pf2e.system.sluggify(this.getAncestryAdjustedSpecialNameLowerCase(name))
|| i.system.slug === game.pf2e.system.sluggify(this.getHeritageAdjustedSpecialNameLowerCase(name))
|| (game.settings.get("pf2e", "dualClassVariant")
&& (i.system.slug === game.pf2e.system.sluggify(this.getDualClassAdjustedSpecialNameLowerCase(name))
)
)
);
if (indexMatch) return indexMatch;
}
return undefined;
}
async #generateFeatItems(compendiumLabel) {
const compendium = await game.packs.get(compendiumLabel);
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
this.parsed.feats.sort((f1, f2) => {
const f1RefUndefined = !(typeof f1.type === "string" || f1.type instanceof String);
const f2RefUndefined = !(typeof f2.type === "string" || f2.type instanceof String);
if (f1RefUndefined || f2RefUndefined) {
if (f1RefUndefined && f2RefUndefined) {
return 0;
} else if (f1RefUndefined) {
return 1;
} else {
return -1;
}
}
return 0;
});
for (const featArray of [this.parsed.feats, this.parsed.specials]) {
for (const pBFeat of featArray) {
if (pBFeat.added) continue;
src_logger.debug("Generating feature for", pBFeat);
const indexMatch = this.#indexFind(index, [pBFeat.name, pBFeat.originalName]);
const displayName = pBFeat.extra ? `${pBFeat.name} (${pBFeat.extra})` : pBFeat.name;
if (!indexMatch) {
src_logger.debug(`Unable to match feat ${displayName}`, { displayName, name: pBFeat.name, extra: pBFeat.extra, pBFeat, compendiumLabel });
this.check[pBFeat.originalName] = { name: displayName, type: "feat", details: { displayName, name: pBFeat.name, originalName: pBFeat.originalName, extra: pBFeat.extra, pBFeat, compendiumLabel } };
continue;
}
if (this.check[pBFeat.originalName]) delete this.check[pBFeat.originalName];
pBFeat.added = true;
if (this.autoAddedFeatureIds.has(`${indexMatch._id}${indexMatch.type}`)) {
src_logger.debug("Feat included in class features auto add", { displayName, pBFeat, compendiumLabel });
continue;
}
const doc = await compendium.getDocument(indexMatch._id);
const item = doc.toObject();
item._id = foundry.utils.randomID();
item.name = displayName;
this.#generateFoundryFeatLocation(item, pBFeat);
this.result.feats.push(item);
await this.#addGrantedItems(item);
}
}
}
async #generateSpecialItems(compendiumLabel) {
const compendium = await game.packs.get(compendiumLabel);
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
for (const special of this.parsed.specials) {
if (special.added) continue;
src_logger.debug("Generating special for", special);
const indexMatch = this.#indexFind(index, [special.name, special.originalName]);
if (!indexMatch) {
src_logger.debug(`Unable to match special ${special.name}`, { special: special.name, compendiumLabel });
this.check[special.originalName] = { name: special.name, type: "special", details: { displayName: special.name, name: special.name, originalName: special.originalName, special } };
continue;
}
special.added = true;
if (this.check[special.originalName]) delete this.check[special.originalName];
if (this.autoAddedFeatureIds.has(`${indexMatch._id}${indexMatch.type}`)) {
src_logger.debug("Special included in class features auto add", { special: special.name, compendiumLabel });
continue;
}
const doc = await compendium.getDocument(indexMatch._id);
const docData = doc.toObject();
docData._id = foundry.utils.randomID();
this.result.feats.push(docData);
await this.#addGrantedItems(docData);
}
}
async #generateEquipmentItems(pack = "pf2e.equipment-srd") {
const compendium = game.packs.get(pack);
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
const compendiumBackpack = await compendium.getDocument("3lgwjrFEsQVKzhh7");
const adventurersPack = this.parsed.equipment.find((e) => e.pbName === "Adventurer's Pack");
const backpackInstance = adventurersPack ? compendiumBackpack.toObject() : null;
if (backpackInstance) {
adventurersPack.added = true;
backpackInstance._id = foundry.utils.randomID();
this.result.adventurersPack.item = adventurersPack;
this.result.equipment.push(backpackInstance);
for (const content of this.result.adventurersPack.contents) {
const indexMatch = index.find((i) => i.system.slug === content.slug);
if (!indexMatch) {
src_logger.error(`Unable to match adventurers kit item ${content.name}`, content);
continue;
}
const doc = await compendium.getDocument(indexMatch._id);
const itemData = doc.toObject();
itemData._id = foundry.utils.randomID();
itemData.system.quantity = content.qty;
itemData.system.containerId = backpackInstance?._id;
this.result.equipment.push(itemData);
}
}
for (const e of this.parsed.equipment) {
if (e.pbName === "Adventurer's Pack") continue;
if (e.added) continue;
if (this.IGNORED_EQUIPMENT.includes(e.pbName)) {
e.added = true;
continue;
}
src_logger.debug("Generating item for", e);
const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(e.pbName));
if (!indexMatch) {
src_logger.error(`Unable to match ${e.pbName}`, e);
this.bad.push({ pbName: e.pbName, type: "equipment", details: { e } });
continue;
}
const doc = await compendium.getDocument(indexMatch._id);
if (doc.type != "kit") {
const itemData = doc.toObject();
itemData._id = foundry.utils.randomID();
itemData.system.quantity = e.qty;
const type = doc.type === "treasure" ? "treasure" : "equipment";
this.result[type].push(itemData);
}
// eslint-disable-next-line require-atomic-updates
e.added = true;
}
}
async #generateWeaponItems() {
const compendium = game.packs.get("pf2e.equipment-srd");
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
for (const w of this.parsed.weapons) {
if (this.IGNORED_EQUIPMENT.includes(w.pbName)) {
w.added = true;
continue;
}
src_logger.debug("Generating weapon for", w);
const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(w.pbName));
if (!indexMatch) {
src_logger.error(`Unable to match weapon item ${w.name}`, w);
this.bad.push({ pbName: w.pbName, type: "weapon", details: { w } });
continue;
}
const doc = await compendium.getDocument(indexMatch._id);
const itemData = doc.toObject();
itemData._id = foundry.utils.randomID();
itemData.system.quantity = w.qty;
itemData.system.damage.die = w.die;
itemData.system.potencyRune.value = w.pot;
itemData.system.strikingRune.value = w.str;
if (w.runes[0]) itemData.system.propertyRune1.value = game.pf2e.system.sluggify(w.runes[0], { camel: "dromedary" });
if (w.runes[1]) itemData.system.propertyRune2.value = game.pf2e.system.sluggify(w.runes[1], { camel: "dromedary" });
if (w.runes[2]) itemData.system.propertyRune3.value = game.pf2e.system.sluggify(w.runes[2], { camel: "dromedary" });
if (w.runes[3]) itemData.system.propertyRune4.value = game.pf2e.system.sluggify(w.runes[3], { camel: "dromedary" });
if (w.mat) {
const material = w.mat.split(" (")[0];
itemData.system.preciousMaterial.value = game.pf2e.system.sluggify(material, { camel: "dromedary" });
itemData.system.preciousMaterialGrade.value = Pathmuncher.getMaterialGrade(w.mat);
}
if (w.display) itemData.name = w.display;
this.result.weapons.push(itemData);
w.added = true;
}
}
async #generateArmorItems() {
const compendium = game.packs.get("pf2e.equipment-srd");
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
for (const a of this.parsed.armor) {
if (this.IGNORED_EQUIPMENT.includes(a.pbName)) {
a.added = true;
continue;
}
src_logger.debug("Generating armor for", a);
const indexMatch = index.find((i) =>
i.system.slug === game.pf2e.system.sluggify(a.pbName)
|| i.system.slug === game.pf2e.system.sluggify(`${a.pbName} Armor`)
);
if (!indexMatch) {
src_logger.error(`Unable to match armor kit item ${a.name}`, a);
this.bad.push({ pbName: a.pbName, type: "armor", details: { a } });
continue;
}
const doc = await compendium.getDocument(indexMatch._id);
const itemData = doc.toObject();
itemData._id = foundry.utils.randomID();
itemData.system.equipped.value = a.worn ?? false;
if (!this.RESTRICTED_EQUIPMENT.some((i) => itemData.name.startsWith(i))) {
itemData.system.equipped.inSlot = a.worn ?? false;
itemData.system.quantity = a.qty;
itemData.system.category = a.prof;
itemData.system.potencyRune.value = a.pot;
itemData.system.resiliencyRune.value = a.res;
const isShield = itemData.system.category === "shield";
itemData.system.equipped.handsHeld = isShield && a.worn ? 1 : 0;
itemData.system.equipped.carryType = isShield && a.worn ? "held" : "worn";
if (a.runes[0]) itemData.system.propertyRune1.value = game.pf2e.system.sluggify(a.runes[0], { camel: "dromedary" });
if (a.runes[1]) itemData.system.propertyRune2.value = game.pf2e.system.sluggify(a.runes[1], { camel: "dromedary" });
if (a.runes[2]) itemData.system.propertyRune3.value = game.pf2e.system.sluggify(a.runes[2], { camel: "dromedary" });
if (a.runes[3]) itemData.system.propertyRune4.value = game.pf2e.system.sluggify(a.runes[3], { camel: "dromedary" });
if (a.mat) {
const material = a.mat.split(" (")[0];
itemData.system.preciousMaterial.value = game.pf2e.system.sluggify(material, { camel: "dromedary" });
itemData.system.preciousMaterialGrade.value = Pathmuncher.getMaterialGrade(a.mat);
}
}
if (a.display) itemData.name = a.display;
this.result.armor.push(itemData);
// eslint-disable-next-line require-atomic-updates
a.added = true;
}
}
getClassSpellCastingType(dual = false) {
const classCaster = dual
? this.source.spellCasters.find((caster) => caster.name === this.source.dualClass)
: this.source.spellCasters.find((caster) => caster.name === this.source.class);
const type = classCaster?.spellcastingType;
if (type || this.source.spellCasters.length === 0) return type ?? "spontaneous";
// if no type and multiple spell casters, then return the first spell casting type
return this.source.spellCasters[0].spellcastingType ?? "spontaneous";
}
// aims to determine the class magic tradition for a spellcasting block
getClassMagicTradition(caster) {
const classCaster = [this.source.class, this.source.dualClass].includes(caster.name);
const tradition = classCaster
? caster?.magicTradition
: undefined;
// if a caster tradition or no spellcasters, return divine
if (tradition || this.source.spellCasters.length === 0) return tradition ?? "divine";
// this spell caster type is not a class, determine class tradition based on ability
const abilityTradition = this.source.spellCasters.find((c) =>
[this.source.class, this.source.dualClass].includes(c.name)
&& c.ability === caster.ability
);
if (abilityTradition) return abilityTradition.magicTradition;
// final fallback
// if no type and multiple spell casters, then return the first spell casting type
return this.source.spellCasters[0].magicTradition && this.source.spellCasters[0].magicTradition !== "focus"
? this.source.spellCasters[0].magicTradition
: "divine";
}
async #generateSpellCaster(caster) {
const isFocus = caster.magicTradition === "focus";
const magicTradition = this.getClassMagicTradition(caster);
const spellcastingType = isFocus ? "focus" : caster.spellcastingType;
const flexible = false; // placeholder
const name = isFocus ? `${src_utils.capitalize(magicTradition)} ${caster.name}` : caster.name;
const spellcastingEntity = {
ability: {
value: caster.ability,
},
proficiency: {
value: caster.proficiency / 2,
},
spelldc: {
item: 0,
},
tradition: {
value: magicTradition,
},
prepared: {
value: spellcastingType,
flexible,
},
slots: {
slot0: {
max: caster.perDay[0],
prepared: {},
value: caster.perDay[0],
},
slot1: {
max: caster.perDay[1],
prepared: {},
value: caster.perDay[1],
},
slot2: {
max: caster.perDay[2],
prepared: {},
value: caster.perDay[2],
},
slot3: {
max: caster.perDay[3],
prepared: {},
value: caster.perDay[3],
},
slot4: {
max: caster.perDay[4],
prepared: {},
value: caster.perDay[4],
},
slot5: {
max: caster.perDay[5],
prepared: {},
value: caster.perDay[5],
},
slot6: {
max: caster.perDay[6],
prepared: {},
value: caster.perDay[6],
},
slot7: {
max: caster.perDay[7],
prepared: {},
value: caster.perDay[7],
},
slot8: {
max: caster.perDay[8],
prepared: {},
value: caster.perDay[8],
},
slot9: {
max: caster.perDay[9],
prepared: {},
value: caster.perDay[9],
},
slot10: {
max: caster.perDay[10],
prepared: {},
value: caster.perDay[10],
},
},
showUnpreparedSpells: { value: true },
};
const data = {
_id: foundry.utils.randomID(),
name,
type: "spellcastingEntry",
system: spellcastingEntity,
};
this.result.casters.push(data);
return data;
}
async #processSpells() {
const compendium = game.packs.get("pf2e.spells-srd");
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
const psiCompendium = game.packs.get("pf2e-psychic-amps.psychic-psi-cantrips");
const psiIndex = psiCompendium ? await psiCompendium.getIndex({ fields: ["name", "type", "system.slug"] }) : undefined;
for (const caster of this.source.spellCasters) {
src_logger.debug("Generating caster for", caster);
if (Number.isInteger(parseInt(caster.focusPoints))) this.result.focusPool += caster.focusPoints;
const instance = await this.#generateSpellCaster(caster);
src_logger.debug("Generated caster instance", instance);
for (const spellSelection of caster.spells) {
const level = spellSelection.spellLevel;
for (const [i, spell] of spellSelection.list.entries()) {
const spellName = spell.split("(")[0].trim();
src_logger.debug("spell details", { spell, spellName, spellSelection, list: spellSelection.list });
const psiMatch = psiIndex ? psiIndex.find((i) => i.name === spell) : undefined;
const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(spellName));
if (!indexMatch && !psiMatch) {
src_logger.error(`Unable to match spell ${spell}`, { spell, spellName, spellSelection, caster, instance });
this.bad.push({ pbName: spell, type: "spell", details: { originalName: spell, name: spellName, spellSelection, caster } });
continue;
}
const doc = psiMatch
? await psiCompendium.getDocument(psiMatch._id)
: await compendium.getDocument(indexMatch._id);
const itemData = doc.toObject();
itemData._id = foundry.utils.randomID();
itemData.system.location.heightenedLevel = level;
itemData.system.location.value = instance._id;
this.result.spells.push(itemData);
instance.system.slots[`slot${level}`].prepared[i] = { id: itemData._id };
}
}
}
setProperty(this.result.character, "system.resources.focus.max", this.result.focusPool);
setProperty(this.result.character, "system.resources.focus.value", this.result.focusPool);
}
async #generateLores() {
for (const lore of this.source.lores) {
const data = {
name: lore[0],
type: "lore",
system: {
proficient: {
value: lore[1] / 2,
},
featType: "",
mod: {
value: 0,
},
item: {
value: 0,
},
},
};
this.result.lores.push(data);
}
}
async #generateMoney() {
const compendium = game.packs.get("pf2e.equipment-srd");
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
const moneyLookup = [
{ slug: "platinum-pieces", type: "pp" },
{ slug: "gold-pieces", type: "gp" },
{ slug: "silver-pieces", type: "sp" },
{ slug: "copper-pieces", type: "cp" },
];
for (const lookup of moneyLookup) {
const indexMatch = index.find((i) => i.system.slug === lookup.slug);
if (indexMatch) {
const doc = await compendium.getDocument(indexMatch._id);
const itemData = doc.toObject();
itemData._id = foundry.utils.randomID();
itemData.system.quantity = this.source.money[lookup.type];
this.result.money.push(itemData);
}
}
}
async #processFormulas() {
const compendium = game.packs.get("pf2e.equipment-srd");
const index = await compendium.getIndex({ fields: ["name", "type", "system.slug"] });
const uuids = [];
for (const formulaSource of this.source.formula) {
for (const formulaName of formulaSource.known) {
const indexMatch = index.find((i) => i.system.slug === game.pf2e.system.sluggify(formulaName));
if (!indexMatch) {
src_logger.error(`Unable to match formula ${formulaName}`, { formulaSource, name: formulaName });
this.bad.push({ pbName: formulaName, type: "formula", details: { formulaSource, name: formulaName } });
continue;
}
const doc = await compendium.getDocument(indexMatch._id);
uuids.push({ uuid: doc.uuid });
}
}
setProperty(this.result.character, "system.crafting.formulas", uuids);
}
async #processFeats() {
await this.#generateFeatItems("pf2e.feats-srd");
await this.#generateFeatItems("pf2e.ancestryfeatures");
await this.#generateSpecialItems("pf2e.ancestryfeatures");
await this.#generateSpecialItems("pf2e.classfeatures");
await this.#generateSpecialItems("pf2e.actionspf2e");
}
async #processEquipment() {
await this.#generateEquipmentItems();
await this.#generateWeaponItems();
await this.#generateArmorItems();
await this.#generateMoney();
}
async #generateTempActor(documents = []) {
const actorData = mergeObject({ type: "character" }, this.result.character);
actorData.name = "Mr Temp";
const actor = await Actor.create(actorData);
const currentState = duplicate(this.result);
const currentItems = [
...(this.options.askForChoices ? this.autoFeats : []),
...currentState.feats,
...currentState.class,
...currentState.background,
...currentState.ancestry,
...currentState.heritage,
...currentState.deity,
...currentState.lores,
];
for (const doc of documents) {
if (!currentItems.some((d) => d._id === doc._id)) {
currentItems.push(doc);
}
}
try {
const items = duplicate(currentItems).map((i) => {
if (i.system.items) i.system.items = [];
if (i.system.rules) i.system.rules = [];
return i;
});
await actor.createEmbeddedDocuments("Item", items, { keepId: true });
const ruleIds = currentItems.map((i) => i._id);
const ruleUpdates = [];
for (const [key, value] of Object.entries(this.allFeatureRules)) {
if (ruleIds.includes(key)) {
ruleUpdates.push({
_id: key,
system: {
// rules: value,
rules: value.filter((r) => ["GrantItem", "ChoiceSet", "RollOption"].includes(r.key)),
},
});
}
}
// console.warn("rule updates", ruleUpdates);
await actor.updateEmbeddedDocuments("Item", ruleUpdates);
const itemUpdates = [];
for (const [key, value] of Object.entries(this.autoAddedFeatureItems)) {
itemUpdates.push({
_id: `${key}`,
system: {
items: deepClone(value),
},
});
}
for (const doc of documents) {
if (getProperty(doc, "system.rules")?.length > 0 && !ruleUpdates.some((r) => r._id === doc._id)) {
ruleUpdates.push({
_id: doc._id,
system: {
rules: deepClone(doc.system.rules),
},
});
}
}
await actor.updateEmbeddedDocuments("Item", itemUpdates);
src_logger.debug("Final temp actor", actor);
} catch (err) {
src_logger.error("Temp actor creation failed", {
actor,
documents,
thisData: deepClone(this.result),
actorData,
err,
currentItems,
this: this,
});
}
return actor;
}
async processCharacter() {
if (!this.source) return;
this.#prepare();
await this.#processCore();
await this.#processFormulas();
await this.#processGenericCompendiumLookup("pf2e.deities", this.source.deity, "deity");
await this.#processGenericCompendiumLookup("pf2e.backgrounds", this.source.background, "background");
await this.#processGenericCompendiumLookup("pf2e.classes", this.source.class, "class");
await this.#processGenericCompendiumLookup("pf2e.ancestries", this.source.ancestry, "ancestry");
await this.#processGenericCompendiumLookup("pf2e.heritages", this.source.heritage, "heritage");
await this.#detectGrantedFeatures();
await this.#processFeats();
await this.#processEquipment();
await this.#processSpells();
await this.#generateLores();
}
async #removeDocumentsToBeUpdated() {
const moneyIds = this.actor.items.filter((i) =>
i.type === "treasure"
&& ["Platinum Pieces", "Gold Pieces", "Silver Pieces", "Copper Pieces"].includes(i.name)
);
const classIds = this.actor.items.filter((i) => i.type === "class").map((i) => i._id);
const deityIds = this.actor.items.filter((i) => i.type === "deity").map((i) => i._id);
const backgroundIds = this.actor.items.filter((i) => i.type === "background").map((i) => i._id);
const heritageIds = this.actor.items.filter((i) => i.type === "heritage").map((i) => i._id);
const ancestryIds = this.actor.items.filter((i) => i.type === "ancestry").map((i) => i._id);
const treasureIds = this.actor.items.filter((i) => i.type === "treasure" && !moneyIds.includes(i.id)).map((i) => i._id);
const featIds = this.actor.items.filter((i) => i.type === "feat").map((i) => i._id);
const actionIds = this.actor.items.filter((i) => i.type === "action").map((i) => i._id);
const equipmentIds = this.actor.items.filter((i) =>
i.type === "equipment" || i.type === "backpack" || i.type === "consumable"
).map((i) => i._id);
const weaponIds = this.actor.items.filter((i) => i.type === "weapon").map((i) => i._id);
const armorIds = this.actor.items.filter((i) => i.type === "armor").map((i) => i._id);
const loreIds = this.actor.items.filter((i) => i.type === "lore").map((i) => i._id);
const spellIds = this.actor.items.filter((i) => i.type === "spell" || i.type === "spellcastingEntry").map((i) => i._id);
const formulaIds = this.actor.system.formulas;
src_logger.debug("ids", {
moneyIds,
deityIds,
classIds,
backgroundIds,
heritageIds,
ancestryIds,
treasureIds,
featIds,
actionIds,
equipmentIds,
weaponIds,
armorIds,
loreIds,
spellIds,
formulaIds,
});
// eslint-disable-next-line complexity
const keepIds = this.actor.items.filter((i) =>
(!this.options.addMoney && moneyIds.includes(i._id))
|| (!this.options.addClass && classIds.includes(i._id))
|| (!this.options.addDeity && deityIds.includes(i._id))
|| (!this.options.addBackground && backgroundIds.includes(i._id))
|| (!this.options.addHeritage && heritageIds.includes(i._id))
|| (!this.options.addAncestry && ancestryIds.includes(i._id))
|| (!this.options.addTreasure && treasureIds.includes(i._id))
|| (!this.options.addFeats && (featIds.includes(i._id) || actionIds.includes(i._id)))
|| (!this.options.addEquipment && equipmentIds.includes(i._id))
|| (!this.options.addWeapons && weaponIds.includes(i._id))
|| (!this.options.addArmor && armorIds.includes(i._id))
|| (!this.options.addLores && loreIds.includes(i._id))
|| (!this.options.addSpells && spellIds.includes(i._id))
).map((i) => i._id);
const deleteIds = this.actor.items.filter((i) => !keepIds.includes(i._id)).map((i) => i._id);
src_logger.debug("ids", {
deleteIds,
keepIds,
});
await this.actor.deleteEmbeddedDocuments("Item", deleteIds);
}
async #createActorEmbeddedDocuments() {
if (this.options.addDeity) await this.actor.createEmbeddedDocuments("Item", this.result.deity, { keepId: true });
if (this.options.addAncestry) await this.actor.createEmbeddedDocuments("Item", this.result.ancestry, { keepId: true });
if (this.options.addHeritage) await this.actor.createEmbeddedDocuments("Item", this.result.heritage, { keepId: true });
if (this.options.addBackground) await this.actor.createEmbeddedDocuments("Item", this.result.background, { keepId: true });
if (this.options.addClass) await this.actor.createEmbeddedDocuments("Item", this.result.class, { keepId: true });
if (this.options.addLores) await this.actor.createEmbeddedDocuments("Item", this.result.lores, { keepId: true });
// for (const feat of this.result.feats.reverse()) {
// console.warn(`creating ${feat.name}`, feat);
// await this.actor.createEmbeddedDocuments("Item", [feat], { keepId: true });
// }
if (this.options.addFeats) await this.actor.createEmbeddedDocuments("Item", this.result.feats, { keepId: true });
if (this.options.addSpells) {
await this.actor.createEmbeddedDocuments("Item", this.result.casters, { keepId: true });
await this.actor.createEmbeddedDocuments("Item", this.result.spells, { keepId: true });
}
if (this.options.addEquipment) await this.actor.createEmbeddedDocuments("Item", this.result.equipment, { keepId: true });
if (this.options.addWeapons) await this.actor.createEmbeddedDocuments("Item", this.result.weapons, { keepId: true });
if (this.options.addArmor) {
await this.actor.createEmbeddedDocuments("Item", this.result.armor, { keepId: true });
await this.actor.updateEmbeddedDocuments("Item", this.result.armor, { keepId: true });
}
if (this.options.addTreasure) await this.actor.createEmbeddedDocuments("Item", this.result.treasure, { keepId: true });
if (this.options.addMoney) await this.actor.createEmbeddedDocuments("Item", this.result.money, { keepId: true });
}
async #restoreEmbeddedRuleLogic() {
const importedItems = this.actor.items.map((i) => i._id);
// Loop back over items and add rule and item progression data back in.
if (!this.options.askForChoices) {
src_logger.debug("Restoring logic", { currentActor: duplicate(this.actor) });
const ruleUpdates = [];
for (const [key, value] of Object.entries(this.autoAddedFeatureRules)) {
if (importedItems.includes(key)) {
ruleUpdates.push({
_id: `${key}`,
system: {
rules: deepClone(value),
},
});
}
}
src_logger.debug("Restoring rule logic", ruleUpdates);
await this.actor.updateEmbeddedDocuments("Item", ruleUpdates);
const itemUpdates = [];
for (const [key, value] of Object.entries(this.autoAddedFeatureItems)) {
if (importedItems.includes(key)) {
itemUpdates.push({
_id: `${key}`,
system: {
items: deepClone(value),
},
});
}
}
src_logger.debug("Restoring granted item logic", itemUpdates);
await this.actor.updateEmbeddedDocuments("Item", itemUpdates);
}
}
async updateActor() {
await this.#removeDocumentsToBeUpdated();
if (!this.options.addName) {
delete this.result.character.name;
delete this.result.character.prototypeToken.name;
}
if (!this.options.addFormulas) {
delete this.result.character.system.formulas;
}
src_logger.debug("Generated result", this.result);
await this.actor.update(this.result.character);
await this.#createActorEmbeddedDocuments();
await this.#restoreEmbeddedRuleLogic();
}
async postImportCheck() {
const badClass = this.options.addClass
? this.bad.filter((b) => b.type === "class").map((b) => `<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" || b.type === "special")
&& this.parsed.feats.concat(this.parsed.specials).some((f) => f.name === b.details.name && !f.added)
).map((b) => `<li>${game.i18n.localize("pathmuncher.Labels.Feats")}: ${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,
...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>`;
}
if (this.result.focusPool > 0) {
warning += `<strong>${game.i18n.localize("pathmuncher.Dialogs.Pathmuncher.FocusSpells")}</strong><br>`;
}
src_logger.debug("Bad thing check", {
badClass,
badAncestry,
badHeritage,
badBackground,
badDeity,
badFeats,
badFeats2,
badEquipment,
badWeapons,
badArmor,
badSpellcasting,
badSpells,
badFamiliars,
badFormulas,
totalBad,
count: totalBad.length,
focusPool: this.result.focusPool,
warning,
});
if (totalBad.length > 0 || this.result.focusPool > 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 ({ parent, pathbuilderJson } = {}) {
this.parent = parent;
this.pathbuilderJson = pathbuilderJson;
this.result = {
pets: [],
features: {},
};
this.bad = {};
this.folders = {};
}
async ensureFolder(type) {
const folderName = game.i18n.localize(`${constants.MODULE_NAME}.Labels.${type}s`);
this.folders[type] = await src_utils.getOrCreateFolder(parent.folder, "Actor", folderName);
}
async #existingPetCheck(familiarName, type) {
const existingPet = game.actors.find((a) =>
a.type === type.toLowerCase()
&& a.name === familiarName
&& a.system.master.id === this.parent._id
);
if (existingPet) return existingPet.toObject();
const actorData = {
type: type.toLowerCase(),
name: familiarName,
system: {
master: {
id: this.parent._id,
ability: this.parent.system.details.keyability.value,
},
},
prototypeToken: {
name: familiarName,
},
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() {
for (const petJson of this.pathbuilderJson.pets) {
// only support familiars at this time
if (petJson.type !== "Familiar") {
src_logger.warn(`Pets with type ${petJson.type} are not supported at this time`);
continue;
}
await this.ensureFolder(petJson.type);
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) {
const localizedType = `pathmuncher.Label.${type}`;
$(".import-progress-bar")
.width(`${Math.trunc((count / total) * 100)}%`)
.html(
`<span>${game.i18n.localize("pathmuncher.Label.Working")} (${game.i18n.localize(localizedType)})...</span>`
);
}
async _updateObject(event, formData) {
const pathbuilderId = formData.textBoxBuildID;
const options = {
pathbuilderId,
addMoney: formData.checkBoxMoney,
addFeats: formData.checkBoxFeats,
addSpells: formData.checkBoxSpells,
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,
askForChoices: formData.checkBoxAskForChoices,
};
src_logger.debug("Pathmuncher options", options);
await src_utils.setFlags(this.actor, options);
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);
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({ 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,
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);
}
}
;// CONCATENATED MODULE: ./src/hooks/sheets.js
function registerSheetButton() {
const trustedUsersOnly = src_utils.setting(constants.SETTINGS.RESTRICT_TO_TRUSTED);
if (trustedUsersOnly && !game.user.isTrusted) return;
/**
* Character sheets
*/
const pcSheetNames = Object.values(CONFIG.Actor.sheetClasses.character)
.map((sheetClass) => sheetClass.cls)
.map((sheet) => sheet.name);
pcSheetNames.forEach((sheetName) => {
Hooks.on("render" + sheetName, (app, html, data) => {
// only for GMs or the owner of this character
if (!data.owner || !data.actor) return;
const button = $(`<a class="pathmuncher-open" title="${constants.MODULE_FULL_NAME}"><i class="fas fa-hat-wizard"></i> Pathmuncher</a>`);
button.click(() => {
const muncher = new PathmuncherImporter(PathmuncherImporter.defaultOptions, data.actor);
muncher.render(true);
});
html.closest('.app').find('.pathmuncher-open').remove();
let titleElement = html.closest('.app').find('.window-title');
if (!app._minimized) button.insertAfter(titleElement);
});
});
}
;// CONCATENATED MODULE: ./src/module.js
Hooks.once("init", () => {
registerSettings();
});
Hooks.once("ready", () => {
registerSheetButton();
registerAPI();
});
/******/ })()
;
//# sourceMappingURL=main.js.map