forked from GitHub-Mirrors/foundry-sw5e

Updated Force and Tech Points on Short and Long Rests along with Temporary points. Also added class powercasting to migration
431 lines
14 KiB
JavaScript
431 lines
14 KiB
JavaScript
/**
|
|
* Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs
|
|
* @return {Promise} A Promise which resolves once the migration is completed
|
|
*/
|
|
export const migrateWorld = async function() {
|
|
ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
|
|
|
|
// Migrate World Actors
|
|
for ( let a of game.actors.entities ) {
|
|
try {
|
|
const updateData = migrateActorData(a.data);
|
|
if ( !isObjectEmpty(updateData) ) {
|
|
console.log(`Migrating Actor entity ${a.name}`);
|
|
await a.update(updateData, {enforceTypes: false});
|
|
}
|
|
} catch(err) {
|
|
err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`;
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
// Migrate World Items
|
|
for ( let i of game.items.entities ) {
|
|
try {
|
|
const updateData = migrateItemData(i.data);
|
|
if ( !isObjectEmpty(updateData) ) {
|
|
console.log(`Migrating Item entity ${i.name}`);
|
|
await i.update(updateData, {enforceTypes: false});
|
|
}
|
|
} catch(err) {
|
|
err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`;
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
// Migrate Actor Override Tokens
|
|
for ( let s of game.scenes.entities ) {
|
|
try {
|
|
const updateData = migrateSceneData(s.data);
|
|
if ( !isObjectEmpty(updateData) ) {
|
|
console.log(`Migrating Scene entity ${s.name}`);
|
|
await s.update(updateData, {enforceTypes: false});
|
|
}
|
|
} catch(err) {
|
|
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
// Migrate World Compendium Packs
|
|
for ( let p of game.packs ) {
|
|
if ( p.metadata.package !== "world" ) continue;
|
|
if ( !["Actor", "Item", "Scene"].includes(p.metadata.entity) ) continue;
|
|
await migrateCompendium(p);
|
|
}
|
|
|
|
// Set the migration as complete
|
|
game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
|
|
ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true});
|
|
};
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Apply migration rules to all Entities within a single Compendium pack
|
|
* @param pack
|
|
* @return {Promise}
|
|
*/
|
|
export const migrateCompendium = async function(pack) {
|
|
const entity = pack.metadata.entity;
|
|
if ( !["Actor", "Item", "Scene"].includes(entity) ) return;
|
|
|
|
// Unlock the pack for editing
|
|
const wasLocked = pack.locked;
|
|
await pack.configure({locked: false});
|
|
|
|
// Begin by requesting server-side data model migration and get the migrated content
|
|
await pack.migrate();
|
|
const content = await pack.getContent();
|
|
|
|
// Iterate over compendium entries - applying fine-tuned migration functions
|
|
for ( let ent of content ) {
|
|
let updateData = {};
|
|
try {
|
|
switch (entity) {
|
|
case "Actor":
|
|
updateData = migrateActorData(ent.data);
|
|
break;
|
|
case "Item":
|
|
updateData = migrateItemData(ent.data);
|
|
break;
|
|
case "Scene":
|
|
updateData = migrateSceneData(ent.data);
|
|
break;
|
|
}
|
|
if ( isObjectEmpty(updateData) ) continue;
|
|
|
|
// Save the entry, if data was changed
|
|
updateData["_id"] = ent._id;
|
|
await pack.updateEntity(updateData);
|
|
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
|
|
}
|
|
|
|
// Handle migration failures
|
|
catch(err) {
|
|
err.message = `Failed sw5e system migration for entity ${ent.name} in pack ${pack.collection}: ${err.message}`;
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
// Apply the original locked status for the pack
|
|
pack.configure({locked: wasLocked});
|
|
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
|
|
};
|
|
|
|
/* -------------------------------------------- */
|
|
/* Entity Type Migration Helpers */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Migrate a single Actor entity to incorporate latest data model changes
|
|
* Return an Object of updateData to be applied
|
|
* @param {object} actor The actor data object to update
|
|
* @return {Object} The updateData to apply
|
|
*/
|
|
export const migrateActorData = function(actor) {
|
|
const updateData = {};
|
|
|
|
// Actor Data Updates
|
|
_migrateActorMovement(actor, updateData);
|
|
_migrateActorSenses(actor, updateData);
|
|
_migrateActorPowers(actor, updateData);
|
|
|
|
// Migrate Owned Items
|
|
if ( !actor.items ) return updateData;
|
|
let hasItemUpdates = false;
|
|
const items = actor.items.map(i => {
|
|
|
|
// Migrate the Owned Item
|
|
let itemUpdate = migrateItemData(i);
|
|
|
|
// Prepared, Equipped, and Proficient for NPC actors
|
|
if ( actor.type === "npc" ) {
|
|
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
|
|
if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
|
|
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
|
|
}
|
|
|
|
// Update the Owned Item
|
|
if ( !isObjectEmpty(itemUpdate) ) {
|
|
hasItemUpdates = true;
|
|
return mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false});
|
|
} else return i;
|
|
});
|
|
if ( hasItemUpdates ) updateData.items = items;
|
|
return updateData;
|
|
};
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template
|
|
* @param {Object} actorData The data object for an Actor
|
|
* @return {Object} The scrubbed Actor data
|
|
*/
|
|
function cleanActorData(actorData) {
|
|
|
|
// Scrub system data
|
|
const model = game.system.model.Actor[actorData.type];
|
|
actorData.data = filterObject(actorData.data, model);
|
|
|
|
// Scrub system flags
|
|
const allowedFlags = CONFIG.SW5E.allowedActorFlags.reduce((obj, f) => {
|
|
obj[f] = null;
|
|
return obj;
|
|
}, {});
|
|
if ( actorData.flags.sw5e ) {
|
|
actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags);
|
|
}
|
|
|
|
// Return the scrubbed data
|
|
return actorData;
|
|
}
|
|
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Migrate a single Item entity to incorporate latest data model changes
|
|
* @param item
|
|
*/
|
|
export const migrateItemData = function(item) {
|
|
const updateData = {};
|
|
_migrateItemClassPowerCasting(item, updateData)
|
|
_migrateItemAttunement(item, updateData);
|
|
return updateData;
|
|
};
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Migrate a single Scene entity to incorporate changes to the data model of it's actor data overrides
|
|
* Return an Object of updateData to be applied
|
|
* @param {Object} scene The Scene data to Update
|
|
* @return {Object} The updateData to apply
|
|
*/
|
|
export const migrateSceneData = function(scene) {
|
|
const tokens = duplicate(scene.tokens);
|
|
return {
|
|
tokens: tokens.map(t => {
|
|
if (!t.actorId || t.actorLink || !t.actorData.data) {
|
|
t.actorData = {};
|
|
return t;
|
|
}
|
|
const token = new Token(t);
|
|
if ( !token.actor ) {
|
|
t.actorId = null;
|
|
t.actorData = {};
|
|
} else if ( !t.actorLink ) {
|
|
const updateData = migrateActorData(token.data.actorData);
|
|
t.actorData = mergeObject(token.data.actorData, updateData);
|
|
}
|
|
return t;
|
|
})
|
|
};
|
|
};
|
|
|
|
/* -------------------------------------------- */
|
|
/* Low level migration utilities
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Migrate the actor speed string to movement object
|
|
* @private
|
|
*/
|
|
function _migrateActorMovement(actorData, updateData) {
|
|
const ad = actorData.data;
|
|
|
|
// Work is needed if old data is present
|
|
const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
|
|
const hasOld = old !== undefined;
|
|
if ( hasOld ) {
|
|
|
|
// If new data is not present, migrate the old data
|
|
const hasNew = ad?.attributes?.movement?.walk !== undefined;
|
|
if ( !hasNew && (typeof old === "string") ) {
|
|
const s = (old || "").split(" ");
|
|
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
|
|
}
|
|
|
|
// Remove the old attribute
|
|
updateData["data.attributes.-=speed"] = null;
|
|
}
|
|
return updateData
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Migrate the actor speed string to movement object
|
|
* @private
|
|
*/
|
|
function _migrateActorPowers(actorData, updateData) {
|
|
const ad = actorData.data;
|
|
|
|
// If new Force & Tech data is not present, create it
|
|
const hasNewAttrib = ad?.attributes?.force?.level !== undefined;
|
|
if ( !hasNewAttrib ) {
|
|
updateData["data.attributes.force.known.value"] = 0;
|
|
updateData["data.attributes.force.known.min"] = 0;
|
|
updateData["data.attributes.force.known.max"] = 0;
|
|
updateData["data.attributes.force.points.value"] = 0;
|
|
updateData["data.attributes.force.points.min"] = 0;
|
|
updateData["data.attributes.force.points.max"] = 0;
|
|
updateData["data.attributes.force.points.temp"] = 0;
|
|
updateData["data.attributes.force.points.tempmax"] = 0;
|
|
updateData["data.attributes.force.level"] = 0;
|
|
updateData["data.attributes.tech.known.value"] = 0;
|
|
updateData["data.attributes.tech.known.min"] = 0;
|
|
updateData["data.attributes.tech.known.max"] = 0;
|
|
updateData["data.attributes.tech.points.value"] = 0;
|
|
updateData["data.attributes.tech.points.min"] = 0;
|
|
updateData["data.attributes.tech.points.max"] = 0;
|
|
updateData["data.attributes.tech.points.temp"] = 0;
|
|
updateData["data.attributes.tech.points.tempmax"] = 0;
|
|
updateData["data.attributes.tech.level"] = 0;
|
|
}
|
|
// If new Bonus Power DC data is not present, create it
|
|
const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined;
|
|
if ( !hasNewBonus ) {
|
|
updateData["data.bonuses.power.forceLightDC"] = "";
|
|
updateData["data.bonuses.power.forceDarkDC"] = "";
|
|
updateData["data.bonuses.power.forceUnivDC"] = "";
|
|
updateData["data.bonuses.power.TechDC"] = "";
|
|
}
|
|
|
|
// Remove the Power DC Bonus
|
|
updateData["data.bonuses.power.-=dc"] = null;
|
|
|
|
return updateData
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Migrate the actor traits.senses string to attributes.senses object
|
|
* @private
|
|
*/
|
|
function _migrateActorSenses(actor, updateData) {
|
|
const ad = actor.data;
|
|
if ( ad?.traits?.senses === undefined ) return;
|
|
const original = ad.traits.senses || "";
|
|
|
|
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
|
|
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
|
|
let wasMatched = false;
|
|
|
|
// Match each comma-separated term
|
|
for ( let s of original.split(",") ) {
|
|
s = s.trim();
|
|
const match = s.match(pattern);
|
|
if ( !match ) continue;
|
|
const type = match[1].toLowerCase();
|
|
if ( type in CONFIG.SW5E.senses ) {
|
|
updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5);
|
|
wasMatched = true;
|
|
}
|
|
}
|
|
|
|
// If nothing was matched, but there was an old string - put the whole thing in "special"
|
|
if ( !wasMatched && !!original ) {
|
|
updateData["data.attributes.senses.special"] = original;
|
|
}
|
|
|
|
// Remove the old traits.senses string once the migration is complete
|
|
updateData["data.traits.-=senses"] = null;
|
|
return updateData;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
function _migrateItemClassPowerCasting(item, updateData) {
|
|
if (item.type === "class"){
|
|
switch (item.name){
|
|
case "Consular":
|
|
updateData["data.powercasting"] = "consular";
|
|
break;
|
|
case "Engineer":
|
|
updateData["data.powercasting"] = "engineer";
|
|
break;
|
|
case "Guardian":
|
|
updateData["data.powercasting"] = "guardian";
|
|
break;
|
|
case "Scout":
|
|
updateData["data.powercasting"] = "scout";
|
|
break;
|
|
case "Sentinel":
|
|
updateData["data.powercasting"] = "sentinel";
|
|
break;
|
|
}
|
|
}
|
|
return updateData;
|
|
}
|
|
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Delete the old data.attuned boolean
|
|
* @private
|
|
*/
|
|
function _migrateItemAttunement(item, updateData) {
|
|
if ( item.data.attuned === undefined ) return;
|
|
updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE;
|
|
updateData["data.-=attuned"] = null;
|
|
return updateData;
|
|
}
|
|
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* A general tool to purge flags from all entities in a Compendium pack.
|
|
* @param {Compendium} pack The compendium pack to clean
|
|
* @private
|
|
*/
|
|
export async function purgeFlags(pack) {
|
|
const cleanFlags = (flags) => {
|
|
const flags5e = flags.sw5e || null;
|
|
return flags5e ? {sw5e: flags5e} : {};
|
|
};
|
|
await pack.configure({locked: false});
|
|
const content = await pack.getContent();
|
|
for ( let entity of content ) {
|
|
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
|
|
if ( pack.entity === "Actor" ) {
|
|
update.items = entity.data.items.map(i => {
|
|
i.flags = cleanFlags(i.flags);
|
|
return i;
|
|
})
|
|
}
|
|
await pack.updateEntity(update, {recursive: false});
|
|
console.log(`Purged flags from ${entity.name}`);
|
|
}
|
|
await pack.configure({locked: true});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Purge the data model of any inner objects which have been flagged as _deprecated.
|
|
* @param {object} data The data to clean
|
|
* @private
|
|
*/
|
|
export function removeDeprecatedObjects(data) {
|
|
for ( let [k, v] of Object.entries(data) ) {
|
|
if ( getType(v) === "Object" ) {
|
|
if (v._deprecated === true) {
|
|
console.log(`Deleting deprecated object key ${k}`);
|
|
delete data[k];
|
|
}
|
|
else removeDeprecatedObjects(v);
|
|
}
|
|
}
|
|
return data;
|
|
}
|