Files
Foundry-VTT-Docker/resources/app/client/data/documents/adventure.js
2025-01-04 00:34:03 +01:00

158 lines
6.5 KiB
JavaScript

/**
* @typedef {Object} AdventureImportData
* @property {Record<string, object[]>} toCreate Arrays of document data to create, organized by document name
* @property {Record<string, object[]>} toUpdate Arrays of document data to update, organized by document name
* @property {number} documentCount The total count of documents to import
*/
/**
* @typedef {Object} AdventureImportResult
* @property {Record<string, Document[]>} created Documents created as a result of the import, organized by document name
* @property {Record<string, Document[]>} updated Documents updated as a result of the import, organized by document name
*/
/**
* The client-side Adventure document which extends the common {@link foundry.documents.BaseAdventure} model.
* @extends foundry.documents.BaseAdventure
* @mixes ClientDocumentMixin
*
* ### Hook Events
* {@link hookEvents.preImportAdventure} emitted by Adventure#import
* {@link hookEvents.importAdventure} emitted by Adventure#import
*/
class Adventure extends ClientDocumentMixin(foundry.documents.BaseAdventure) {
/** @inheritdoc */
static fromSource(source, options={}) {
const pack = game.packs.get(options.pack);
if ( pack && !pack.metadata.system ) {
// Omit system-specific documents from this Adventure's data.
source.actors = [];
source.items = [];
source.folders = source.folders.filter(f => !CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES.includes(f.type));
}
return super.fromSource(source, options);
}
/* -------------------------------------------- */
/**
* Perform a full import workflow of this Adventure.
* Create new and update existing documents within the World.
* @param {object} [options] Options which configure and customize the import process
* @param {boolean} [options.dialog=true] Display a warning dialog if existing documents would be overwritten
* @returns {Promise<AdventureImportResult>} The import result
*/
async import({dialog=true, ...importOptions}={}) {
const importData = await this.prepareImport(importOptions);
// Allow modules to preprocess adventure data or to intercept the import process
const allowed = Hooks.call("preImportAdventure", this, importOptions, importData.toCreate, importData.toUpdate);
if ( allowed === false ) {
console.log(`"${this.name}" Adventure import was prevented by the "preImportAdventure" hook`);
return {created: [], updated: []};
}
// Warn the user if the import operation will overwrite existing World content
if ( !foundry.utils.isEmpty(importData.toUpdate) && dialog ) {
const confirm = await Dialog.confirm({
title: game.i18n.localize("ADVENTURE.ImportOverwriteTitle"),
content: `<h4><strong>${game.i18n.localize("Warning")}:</strong></h4>
<p>${game.i18n.format("ADVENTURE.ImportOverwriteWarning", {name: this.name})}</p>`
});
if ( !confirm ) return {created: [], updated: []};
}
// Perform the import
const {created, updated} = await this.importContent(importData);
// Refresh the sidebar display
ui.sidebar.render();
// Allow modules to perform additional post-import workflows
Hooks.callAll("importAdventure", this, importOptions, created, updated);
// Update the imported state of the adventure.
const imports = game.settings.get("core", "adventureImports");
imports[this.uuid] = true;
await game.settings.set("core", "adventureImports", imports);
return {created, updated};
}
/* -------------------------------------------- */
/**
* Prepare Adventure data for import into the World.
* @param {object} [options] Options passed in from the import dialog to configure the import
* behavior.
* @param {string[]} [options.importFields] A subset of adventure fields to import.
* @returns {Promise<AdventureImportData>}
*/
async prepareImport({ importFields=[] }={}) {
importFields = new Set(importFields);
const adventureData = this.toObject();
const toCreate = {};
const toUpdate = {};
let documentCount = 0;
const importAll = !importFields.size || importFields.has("all");
const keep = new Set();
for ( const [field, cls] of Object.entries(Adventure.contentFields) ) {
if ( !importAll && !importFields.has(field) ) continue;
keep.add(cls.documentName);
const collection = game.collections.get(cls.documentName);
let [c, u] = adventureData[field].partition(d => collection.has(d._id));
if ( (field === "folders") && !importAll ) {
c = c.filter(f => keep.has(f.type));
u = u.filter(f => keep.has(f.type));
}
if ( c.length ) {
toCreate[cls.documentName] = c;
documentCount += c.length;
}
if ( u.length ) {
toUpdate[cls.documentName] = u;
documentCount += u.length;
}
}
return {toCreate, toUpdate, documentCount};
}
/* -------------------------------------------- */
/**
* Execute an Adventure import workflow, creating and updating documents in the World.
* @param {AdventureImportData} data Prepared adventure data to import
* @returns {Promise<AdventureImportResult>} The import result
*/
async importContent({toCreate, toUpdate, documentCount}={}) {
const created = {};
const updated = {};
// Display importer progress
const importMessage = game.i18n.localize("ADVENTURE.ImportProgress");
let nImported = 0;
SceneNavigation.displayProgressBar({label: importMessage, pct: 1});
// Create new documents
for ( const [documentName, createData] of Object.entries(toCreate) ) {
const cls = getDocumentClass(documentName);
const docs = await cls.createDocuments(createData, {keepId: true, keepEmbeddedId: true, renderSheet: false});
created[documentName] = docs;
nImported += docs.length;
SceneNavigation.displayProgressBar({label: importMessage, pct: Math.floor(nImported * 100 / documentCount)});
}
// Update existing documents
for ( const [documentName, updateData] of Object.entries(toUpdate) ) {
const cls = getDocumentClass(documentName);
const docs = await cls.updateDocuments(updateData, {diff: false, recursive: false, noHook: true});
updated[documentName] = docs;
nImported += docs.length;
SceneNavigation.displayProgressBar({label: importMessage, pct: Math.floor(nImported * 100 / documentCount)});
}
SceneNavigation.displayProgressBar({label: importMessage, pct: 100});
return {created, updated};
}
}