|
import CONFIGURATION from "./adventure.mjs";
|
|
|
|
/**
|
|
* @typedef {Object} LocalizationData
|
|
* @property {Set<string>} html HTML files which provide Journal Entry page translations
|
|
* @property {object} i18n An object of localization keys and translation strings
|
|
*/
|
|
|
|
/**
|
|
* A subclass of the core AdventureImporter which performs some special functions for Pathfinder premium content.
|
|
*/
|
|
export default class PF2EAdventureImporter extends AdventureImporter {
|
|
constructor(adventure, options) {
|
|
super(adventure, options);
|
|
this.config = CONFIGURATION.adventure;
|
|
this.options.classes.push(CONFIGURATION.cssClass);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritDoc */
|
|
async getData(options={}) {
|
|
return foundry.utils.mergeObject(await super.getData(), {
|
|
importOptions: this.config.importOptions || {}
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritDoc */
|
|
async _renderInner(data) {
|
|
const html = await super._renderInner(data);
|
|
if ( !this.config.importOptions ) return html;
|
|
|
|
// Insert import controls.
|
|
const imported = game.settings.get(CONFIGURATION.moduleId, "imported");
|
|
if ( imported ) this.#addImportControls(html.find(".adventure-contents")[0]);
|
|
|
|
// Insert options and return
|
|
html.find(".adventure-contents").append(this.#formatOptions());
|
|
return html;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Format adventure import options block.
|
|
* @returns {string}
|
|
*/
|
|
#formatOptions() {
|
|
let options = `<section class="import-form"><h2>Importer Options</h2>`;
|
|
for ( const [name, option] of Object.entries(this.config.importOptions) ) {
|
|
options += `<div class="form-group">
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="${name}" title="${option.label}" ${option.default ? "checked" : ""}/>
|
|
${option.label}
|
|
</label>
|
|
</div>`;
|
|
}
|
|
options += `</section>`;
|
|
return options;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Add controls for which content to import.
|
|
* @param {HTMLElement} content The adventure content container.
|
|
*/
|
|
#addImportControls(content) {
|
|
const heading = content.querySelector("h2");
|
|
const list = content.querySelector("ul");
|
|
const section = document.createElement("section");
|
|
section.classList.add("import-controls");
|
|
let html = `
|
|
<div class="form-group">
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="importFields" value="all" title="Import All" checked>
|
|
Import All
|
|
</label>
|
|
</div>
|
|
`;
|
|
for (const [field, cls] of Object.entries(Adventure.contentFields)) {
|
|
const count = this.object[field].size;
|
|
if ( !count ) continue;
|
|
const label = game.i18n.localize(count > 1 ? cls.metadata.labelPlural : cls.metadata.label);
|
|
html += `
|
|
<div class="form-group">
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="importFields" value="${field}" title="Import ${label}"
|
|
checked disabled>
|
|
<i class="${CONFIG[cls.documentName].sidebarIcon}"></i>
|
|
${count} ${label}
|
|
</label>
|
|
</div>
|
|
`;
|
|
}
|
|
section.innerHTML = html;
|
|
section.insertAdjacentElement("afterbegin", heading);
|
|
list.before(section);
|
|
list.remove();
|
|
section.querySelector('[value="all"]').addEventListener("change", event => {
|
|
this.#onToggleImportAll(event);
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Handle toggling the import all checkbox.
|
|
* @param {Event} event The change event.
|
|
*/
|
|
#onToggleImportAll(event) {
|
|
const target = event.currentTarget;
|
|
const section = target.closest(".import-controls");
|
|
const checked = target.checked;
|
|
section.querySelectorAll("input").forEach(input => {
|
|
if ( input.value !== "folders" ) input.disabled = checked;
|
|
if ( checked ) input.checked = true;
|
|
});
|
|
target.disabled = false;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritDoc */
|
|
async _prepareImportData(formData) {
|
|
this.submitOptions = formData;
|
|
const {toCreate, toUpdate, documentCount} = await super._prepareImportData(formData);
|
|
this.#applyImportControls(formData, toCreate, toUpdate);
|
|
this.#applyEnhancedMapsPreference(formData.enhancedMaps, toCreate, toUpdate);
|
|
|
|
// Prepare localization data
|
|
const localization = await this.#prepareLocalizationData();
|
|
|
|
// Merge Compendium Actor data
|
|
if ( "Actor" in toCreate ) await this.#mergeCompendiumActors(toCreate.Actor, formData);
|
|
if ( "Actor" in toUpdate ) await this.#mergeCompendiumActors(toUpdate.Actor, formData);
|
|
|
|
// Merge Journal HTML data
|
|
if ( "JournalEntry" in toCreate ) await this.#mergeJournalHTML(toCreate.JournalEntry, localization);
|
|
if ( "JournalEntry" in toUpdate ) await this.#mergeJournalHTML(toUpdate.JournalEntry, localization);
|
|
|
|
// Apply localized translations
|
|
await this.#applyTranslations(toCreate, localization);
|
|
await this.#applyTranslations(toUpdate, localization);
|
|
return {toCreate, toUpdate, documentCount};
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritDoc */
|
|
async _importContent(toCreate, toUpdate, documentCount) {
|
|
const importResult = await super._importContent(toCreate, toUpdate, documentCount);
|
|
for ( let [name, option] of Object.entries(this.config.importOptions || {}) ) {
|
|
if ( !option.handler ) continue;
|
|
await option.handler(this.document, option, this.submitOptions[name]);
|
|
}
|
|
return importResult;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Pre-Import Customizations */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Get available localization data which can be used during the import process
|
|
* @returns {Promise<LocalizationData>}
|
|
*/
|
|
async #prepareLocalizationData() {
|
|
const path = `modules/${CONFIGURATION.moduleId}/lang/${game.i18n.lang}/${this.config.slug}`;
|
|
if ( game.i18n.lang === "en" ) return {path, i18n: {}, html: new Set()};
|
|
const json = `${path}/${this.config.slug}.json`;
|
|
try {
|
|
const files = (await FilePicker.browse("data", path)).files;
|
|
const i18n = files.includes(json) ? await fetch(json).then(r => r.json()) : {};
|
|
const html = new Set(files.filter(f => f.endsWith(".html")));
|
|
return {path, i18n, html};
|
|
} catch(err) {
|
|
return {path, i18n: {}, html: new Set()};
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Merge Actor data with authoritative source data from system compendium packs
|
|
* @param {Actor[]} actors Actor documents intended to be imported
|
|
* @param {object} importOptions Form submission import options
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async #mergeCompendiumActors(actors, importOptions) {
|
|
for ( const actor of actors ) {
|
|
const sourceId = actor.flags?.core?.sourceId;
|
|
if ( !sourceId ) {
|
|
console.warn(`[${CONFIGURATION.moduleId}] Actor "${actor.name}" [${actor._id}] had no `
|
|
+ "sourceId to retrieve source data from.");
|
|
continue;
|
|
}
|
|
const source = await fromUuid(sourceId);
|
|
if ( source ) {
|
|
const {system, items, effects} = source.toObject();
|
|
const updateData = {
|
|
system, items, effects,
|
|
"flags.core.sourceId": source.uuid
|
|
};
|
|
const overrides = this.config.actorOverrides[actor._id] || [];
|
|
for ( const field of overrides ) delete updateData[field];
|
|
foundry.utils.mergeObject(actor, updateData);
|
|
} else {
|
|
const [, scope, packName] = sourceId?.split(".") ?? [];
|
|
console.warn(`[${CONFIGURATION.moduleId}] Compendium source data for "${actor.name}" `
|
|
+ `[${actor._id}] not found in pack ${scope}.${packName}.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Merge JournalEntry data with localized source HTML.
|
|
* @param {JournalEntry[]} entries JournalEntry documents intended to be imported
|
|
* @param {LocalizationData} localization Localization configuration data
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async #mergeJournalHTML(entries, localization) {
|
|
for ( const entry of entries ) {
|
|
for ( const page of entry.pages ) {
|
|
const htmlFile = `${localization.path}/${page._id}-${page.name.slugify({strict: true})}.html`;
|
|
if ( localization.html.has(htmlFile) ) {
|
|
const content = await fetch(htmlFile).then(r => r.text()).catch(err => null);
|
|
if ( content ) foundry.utils.mergeObject(page, {"text.content": content});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Apply localization translations to documents prior to import.
|
|
* @param {Object<string,Document[]>} group A group of documents to be created or updated
|
|
* @param {LocalizationData} localization Localization configuration data
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async #applyTranslations(group, localization) {
|
|
for ( const [documentName, documents] of Object.entries(group) ) {
|
|
const translations = localization.i18n[documentName] || [];
|
|
for ( const document of documents ) {
|
|
const translation = translations.find(d => d._id === document._id);
|
|
if ( translation ) foundry.utils.mergeObject(document, translation);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Remove adventure content that the user indicated they did not want to import.
|
|
* @param {object} formData The submitted adventure form data.
|
|
* @param {object} toCreate An object of document data to create.
|
|
* @param {object} toUpdate An object of document data to update.
|
|
*/
|
|
#applyImportControls(formData, toCreate, toUpdate) {
|
|
if ( !game.settings.get(CONFIGURATION.moduleId, "imported") ) return;
|
|
const fields = formData.importFields.filter(_ => _);
|
|
fields.push("folders");
|
|
if ( !fields || !Array.isArray(fields) || fields.some(field => field === "all") ) return;
|
|
const keep = new Set(fields.map(field => Adventure.contentFields[field].documentName));
|
|
[toCreate, toUpdate].forEach(docs => {
|
|
for ( const type of Object.keys(docs) ) {
|
|
if ( !keep.has(type) ) delete docs[type];
|
|
}
|
|
if ( docs.Folder ) docs.Folder = docs.Folder.filter(f => keep.has(f.type));
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Remove scenes from the import depending on whether the user wants only the enhanced maps or
|
|
* only the original ones.
|
|
* @param {boolean} useEnhanced Whether to import enhanced or original maps.
|
|
* @param {object} toCreate An object of document data to create.
|
|
* @param {object} toUpdate An object of document data to update.
|
|
*/
|
|
#applyEnhancedMapsPreference(useEnhanced, toCreate, toUpdate) {
|
|
const sceneIds = this.config.importOptions.enhancedMaps.sceneIds;
|
|
const affectedScenes = new Set(sceneIds.original.concat(sceneIds.enhanced));
|
|
const original = new Set(sceneIds.original);
|
|
const enhanced = new Set(sceneIds.enhanced);
|
|
[toCreate, toUpdate].forEach(docs => {
|
|
if ( docs.Scene ) docs.Scene = docs.Scene.filter(s => {
|
|
if ( !affectedScenes.has(s._id) ) return true;
|
|
return (useEnhanced && enhanced.has(s._id)) || (!useEnhanced && original.has(s._id));
|
|
});
|
|
});
|
|
}
|
|
}
|