Files
l5rx-chiaroscuro/system/scripts/item.js
2025-05-01 14:41:43 +02:00

315 lines
9.7 KiB
JavaScript

export class ItemL5r5e extends Item {
/**
* A reference to the Collection of embedded Item instances in the document, indexed by _id.
* @returns {Collection<BaseItem>}
*/
get items() {
return this.system.items || new Map();
}
/**
* Return the linked Actor instance if any (current or embed)
* @return {Actor|null}
*/
get actor() {
return super.actor || game.actors.get(this.system.parent_id?.actor_id) || null;
}
/**
* A Universally Unique Identifier (uuid) for this Document instance patched for embedded items on items
* @type {string}
* @memberof ClientDocumentMixin#
*/
get uuid() {
const parents = this.system.parent_id;
if (!parents?.item_id) {
return super.uuid;
}
// Embedded item
const uuid = [];
if (parents?.actor_id) {
uuid.push(`Actor.${parents.actor_id}`);
}
uuid.push(`Item.${parents.item_id}`);
uuid.push(`Item.${this._id}`);
return uuid.join(".");
}
/**
* Obtain a reference to the Array of source data within the data object for a certain embedded Document name
*
* TODO probably useless if we can add "items" in metadata.embedded, but no clue how to.
*
* @param {string} embeddedName The name of the embedded Document type
* @return {DocumentCollection} The Collection instance of embedded Documents of the requested type
*/
getEmbeddedCollection(embeddedName) {
if (embeddedName === "Item") {
return this.items;
}
return super.getEmbeddedCollection(embeddedName);
}
/**
* Create a new entity using provided input data
* @override
*/
static async create(data, context = {}) {
if (data.img === undefined) {
data.img = `${CONFIG.l5r5e.paths.assets}icons/items/${data.type}.svg`;
}
return super.create(data, context);
}
/**
* Update this Document using incremental data, saving it to the database.
* @see {@link Document.updateDocuments}
* @param {object} [data={}] Differential update data which modifies the existing values of this document data
* @param {DocumentModificationContext} [context={}] Additional context which customizes the update workflow
* @returns {Promise<Document>} The updated Document instance
*/
async update(data = {}, context = {}) {
// Regular
if (!this.system.parent_id) {
return super.update(data, context);
}
// **** Embed Items, need to get the parents ****
const parentItem = this.getItemFromParentId();
if (!parentItem) {
console.warn(`L5R5E | Helpers | Embed parentItem not found`);
return;
}
// Merge (DocumentData cannot be set)
const result = foundry.utils.mergeObject(this, foundry.utils.expandObject(data));
if (result.name) {
this.name = result.name;
}
if (result.img) {
this.img = result.img;
}
if (result.system) {
this.system = result.system;
}
// Update
await parentItem.updateEmbedItem(this.toObject(false));
// Return new value for sheet
return new Promise((resolve) => resolve(this));
}
/** @override */
prepareData() {
super.prepareData();
// Prepare Embed items
if (!(this.system.items instanceof Map)) {
const itemsData = Array.isArray(this.system.items) ? this.system.items : [];
this.system.items = new Map();
itemsData.forEach((item) => {
this.addEmbedItem(item, { save: false, newId: false, addBonusToActor: false });
});
}
// Sanitize some values
switch (this.type) {
case "armor":
this.system.armor.physical = this.system.armor.physical || 0;
this.system.armor.supernatural = this.system.armor.supernatural || 0;
break;
case "weapon":
this.system.range = this.system.range || 0;
this.system.damage = this.system.damage || 0;
this.system.deadliness = this.system.deadliness || 0;
break;
}
}
// ***** parent ids management *****
/**
* Return a string with idemId + actorId if any
* @return {{item_id: (string|null), actor_id?: (string|null)}}
*/
getParentsIds() {
const parent = {
item_id: this.id,
};
if (this.actor?._id) {
parent.actor_id = this.actor._id;
}
return parent;
}
/**
* Return the Item Object for the "parentId"
* @return {ItemL5r5e|null}
*/
getItemFromParentId() {
const parentIds = this.system.parent_id;
let parentItem;
if (parentIds?.actor_id) {
// Actor item object
const parentActor = parentIds.actor_id ? game.actors.get(parentIds.actor_id) : null;
parentItem = parentActor?.items.get(parentIds.item_id);
} else if (parentIds?.item_id) {
// World Object
parentItem = game.items.get(parentIds.item_id);
}
return parentItem;
}
/**
* Render the text template for this Item (tooltips and chat)
* @return {Promise<string|null>}
*/
async renderTextTemplate() {
const sheetData = (await this.sheet?.getData()) || this;
if (sheetData instanceof ItemL5r5e) {
await game.l5r5e.HelpersL5r5e.refreshItemProperties(this);
}
const type = this.type.replace("_", "-"); // ex: item_pattern
const tpl = await foundry.applications.handlebars.renderTemplate(`${CONFIG.l5r5e.paths.templates}items/${type}/${type}-text.html`, sheetData);
if (!tpl) {
return null;
}
return tpl;
}
// ***** Embedded items management *****
/**
* Shortcut for this.items.get
* @param id
* @return {ItemL5r5e|null}
*/
getEmbedItem(id) {
return this.items?.get(id) || null;
}
/**
* Add an Embed Item
* @param {ItemL5r5e} item Object to add
* @param {boolean} save if we save in db or not (used internally)
* @param {boolean} newId if we change the id
* @param {boolean} addBonusToActor if we update the actor bonus for advancements
* @return {Promise<string>}
*/
async addEmbedItem(item, { save = true, newId = true, addBonusToActor = true } = {}) {
if (!item) {
return;
}
if (!(item instanceof Item) && item?.name && item?.type) {
// Data -> Item
item = new ItemL5r5e(item);
}
// New id
if (newId || !item._id) {
// Bypass the readonly for "_id"
const tmpData = item.toJSON();
tmpData._id = foundry.utils.randomID();
item = new ItemL5r5e(tmpData);
}
// Copy the parent permission to the sub item
// In v10 actor's items inherit the ownership from the actor, but theirs ownership do not reflect that.
// So we must take actor's ownership for sub-item
item.ownership = this.actor?.ownership ?? this.ownership;
// Tag parent (flags won't work as we have no id in db)
item.system.parent_id = this.getParentsIds();
// Object
this.system.items.set(item._id, item);
// Add bonus to actor
if (addBonusToActor) {
const actor = this.actor;
if (item instanceof Item && actor instanceof Actor) {
actor.addBonus(item);
}
}
if (save) {
await this.saveEmbedItems();
}
return item._id;
}
/**
* Update an Embed Item
* @param {ItemL5r5e} item Object to add
* @param {boolean} save if we save in db or not (used internally)
* @return {Promise<string>}
*/
async updateEmbedItem(item, { save = true } = {}) {
return await this.addEmbedItem(item, { save, newId: false, addBonusToActor: false });
}
/**
* Delete the Embed Item and clear the actor bonus if any
* @param id Item id
* @param {boolean} save if we save in db or not (used internally)
* @param {boolean} removeBonusFromActor if we update the actor bonus for advancements
* @return {Promise<void>}
*/
async deleteEmbedItem(id, { save = true, removeBonusFromActor = true } = {}) {
if (!this.system.items.has(id)) {
return;
}
// Remove bonus from actor
if (removeBonusFromActor) {
const actor = this.actor;
const item = this.system.items.get(id);
if (item instanceof Item && actor instanceof Actor) {
actor.removeBonus(item);
}
}
// Remove the embed item
this.system.items.delete(id);
if (save) {
await this.saveEmbedItems();
}
}
/**
* Generate new Ids for the embed items
* @return {Promise<void>}
*/
async generateNewIdsForAllEmbedItems() {
// Clear olds ids
const oldItems = Array.from(this.system.items);
this.system.items = new Map();
// Re-add with new ids
oldItems.forEach(([id, item]) => {
this.addEmbedItem(item, { save: false, newId: true, addBonusToActor: false });
});
return this.saveEmbedItems();
}
/**
* Save all the Embed Items
* @return {Promise<void>}
*/
async saveEmbedItems() {
await this.update({
"system.items": Array.from(this.system.items).map(([id, item]) => item.toObject(false)),
});
this.sheet.render(false);
}
}