Files
Foundry-VTT-Docker/resources/app/common/documents/actor-delta.mjs

177 lines
6.1 KiB
JavaScript
Raw Normal View History

2025-01-04 00:34:03 +01:00
import Document from "../abstract/document.mjs";
import {deepClone, mergeObject} from "../utils/helpers.mjs";
import * as documents from "./_module.mjs";
import * as fields from "../data/fields.mjs";
import BaseActor from "./actor.mjs";
/**
* @typedef {import("./_types.mjs").ActorDeltaData} ActorDeltaData
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
*/
/**
* The ActorDelta Document.
* Defines the DataSchema and common behaviors for an ActorDelta which are shared between both client and server.
* ActorDeltas store a delta that can be applied to a particular Actor in order to produce a new Actor.
* @mixes ActorDeltaData
*/
export default class BaseActorDelta extends Document {
/**
* Construct an ActorDelta document using provided data and context.
* @param {Partial<ActorDeltaData>} data Initial data used to construct the ActorDelta.
* @param {DocumentConstructionContext} context Construction context options.
*/
constructor(data, context) {
super(data, context);
}
/* -------------------------------------------- */
/* Model Configuration */
/* -------------------------------------------- */
/** @inheritdoc */
static metadata = Object.freeze(mergeObject(super.metadata, {
name: "ActorDelta",
collection: "delta",
label: "DOCUMENT.ActorDelta",
labelPlural: "DOCUMENT.ActorDeltas",
isEmbedded: true,
embedded: {
Item: "items",
ActiveEffect: "effects"
},
schemaVersion: "12.324"
}, {inplace: false}));
/** @override */
static defineSchema() {
return {
_id: new fields.DocumentIdField(),
name: new fields.StringField({required: false, nullable: true, initial: null}),
type: new fields.StringField({required: false, nullable: true, initial: null}),
img: new fields.FilePathField({categories: ["IMAGE"], nullable: true, initial: null, required: false}),
system: new fields.ObjectField(),
items: new fields.EmbeddedCollectionDeltaField(documents.BaseItem),
effects: new fields.EmbeddedCollectionDeltaField(documents.BaseActiveEffect),
ownership: new fields.DocumentOwnershipField({required: false, nullable: true, initial: null}),
flags: new fields.ObjectField()
};
}
/* -------------------------------------------- */
/** @override */
canUserModify(user, action, data={}) {
return this.parent.canUserModify(user, action, data);
}
/* -------------------------------------------- */
/** @override */
testUserPermission(user, permission, { exact=false }={}) {
return this.parent.testUserPermission(user, permission, { exact });
}
/* -------------------------------------------- */
/* Methods */
/* -------------------------------------------- */
/**
* Retrieve the base actor's collection, if it exists.
* @param {string} collectionName The collection name.
* @returns {Collection}
*/
getBaseCollection(collectionName) {
const baseActor = this.parent?.baseActor;
return baseActor?.getEmbeddedCollection(collectionName);
}
/* -------------------------------------------- */
/**
* Apply an ActorDelta to an Actor and return the resultant synthetic Actor.
* @param {ActorDelta} delta The ActorDelta.
* @param {Actor} baseActor The base Actor.
* @param {object} [context] Context to supply to synthetic Actor instantiation.
* @returns {Actor|null}
*/
static applyDelta(delta, baseActor, context={}) {
if ( !baseActor ) return null;
if ( delta.parent?.isLinked ) return baseActor;
// Get base actor data.
const cls = game?.actors?.documentClass ?? db.Actor;
const actorData = baseActor.toObject();
const deltaData = delta.toObject();
delete deltaData._id;
// Merge embedded collections.
BaseActorDelta.#mergeEmbeddedCollections(cls, actorData, deltaData);
// Merge the rest of the delta.
mergeObject(actorData, deltaData);
return new cls(actorData, {parent: delta.parent, ...context});
}
/* -------------------------------------------- */
/**
* Merge delta Document embedded collections with the base Document.
* @param {typeof Document} documentClass The parent Document class.
* @param {object} baseData The base Document data.
* @param {object} deltaData The delta Document data.
*/
static #mergeEmbeddedCollections(documentClass, baseData, deltaData) {
for ( const collectionName of Object.keys(documentClass.hierarchy) ) {
const baseCollection = baseData[collectionName];
const deltaCollection = deltaData[collectionName];
baseData[collectionName] = BaseActorDelta.#mergeEmbeddedCollection(baseCollection, deltaCollection);
delete deltaData[collectionName];
}
}
/* -------------------------------------------- */
/**
* Apply an embedded collection delta.
* @param {object[]} base The base embedded collection.
* @param {object[]} delta The delta embedded collection.
* @returns {object[]}
*/
static #mergeEmbeddedCollection(base=[], delta=[]) {
const deltaIds = new Set();
const records = [];
for ( const record of delta ) {
if ( !record._tombstone ) records.push(record);
deltaIds.add(record._id);
}
for ( const record of base ) {
if ( !deltaIds.has(record._id) ) records.push(record);
}
return records;
}
/* -------------------------------------------- */
/** @override */
static migrateData(source) {
return BaseActor.migrateData(source);
}
/* -------------------------------------------- */
/* Serialization */
/* -------------------------------------------- */
/** @override */
toObject(source=true) {
const data = {};
const value = source ? this._source : this;
for ( const [name, field] of this.schema.entries() ) {
const v = value[name];
if ( !field.required && ((v === undefined) || (v === null)) ) continue; // Drop optional fields
data[name] = source ? deepClone(value[name]) : field.toObject(value[name]);
}
return data;
}
}