|
/******/ (() => { // 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
|