/** * Extends the actor to process special things from L5R. */ export class ActorL5r5e extends Actor { /** * Create a new entity using provided input data * @override */ static async create(docData, options = {}) { // if (!Object.keys(docData).includes("type")) { // data.type = "character"; // } // Replace default image if (docData.img === undefined) { docData.img = `${CONFIG.l5r5e.paths.assets}icons/actors/${docData.type}.svg`; } // Some tweak on actors prototypeToken docData.prototypeToken = docData.prototypeToken || {}; switch (docData.type) { case "character": foundry.utils.mergeObject( docData.prototypeToken, { // vision: true, // dimSight: 30, // brightSight: 0, actorLink: true, disposition: 1, // friendly bar1: { attribute: "fatigue", }, bar2: { attribute: "strife", }, }, { overwrite: false } ); break; case "npc": foundry.utils.mergeObject( docData.prototypeToken, { actorLink: true, disposition: 0, // neutral bar1: { attribute: "fatigue", }, bar2: { attribute: "strife", }, }, { overwrite: false } ); break; case "army": foundry.utils.mergeObject( docData.prototypeToken, { actorLink: true, disposition: 0, // neutral bar1: { attribute: "battle_readiness.casualties_strength", }, bar2: { attribute: "battle_readiness.panic_discipline", }, }, { overwrite: false } ); break; } await super.create(docData, options); } /** * Entity-specific actions that should occur when the Entity is updated * @override */ async update(docData = {}, context = {}) { // fix foundry v0.8.8 (config token=object, update=flat array) docData = foundry.utils.flattenObject(docData); // Need a _id if (!docData["_id"]) { docData["_id"] = this.id; } // Context informations (needed for unlinked token update) context.parent = this.parent; context.pack = this.pack; // NPC switch between types : Linked actor for Adversary, unlinked for Minion if (!!docData["system.type"] && this.type === "npc" && docData["system.type"] !== this.system.type) { docData["prototypeToken.actorLink"] = docData["system.type"] === "adversary"; } // Only on linked Actor if ( !!docData["prototypeToken.actorLink"] || (docData["prototypeToken.actorLink"] === undefined && this.prototypeToken?.actorLink) ) { // Update the token name/image if the sheet name/image changed, but only if // they was previously the same, and token img was not set in same time Object.entries({ name: "name", img: "texture.src" }).forEach(([dataProp, TknProp]) => { if ( docData[dataProp] && !docData["prototypeToken." + TknProp] && this[dataProp] === foundry.utils.getProperty(this.prototypeToken, TknProp) && this[dataProp] !== docData[dataProp] ) { docData["prototypeToken." + TknProp] = docData[dataProp]; } }); } // Now using updateDocuments return Actor.updateDocuments([docData], context).then(() => { // Notify the "Gm Monitor" if this actor is watched if (game.settings.get("l5r5e", "gm-monitor-actors").find((e) => e === this.id)) { game.l5r5e.HelpersL5r5e.refreshLocalAndSocket("l5r5e-gm-monitor"); } }); } /** @override */ prepareData() { super.prepareData(); if (this.isCharacter) { const system = this.system; // No automation for npc as they cheat in stats if (this.type === "character") { ActorL5r5e.computeDerivedAttributes(system); } // Attributes bars system.fatigue.max = system.endurance; system.strife.max = system.composure; system.void_points.max = system.rings.void; // if compromise, vigilance = 1 system.is_compromised = system.strife.value > system.strife.max; // Make sure void points are never greater than max if (system.void_points.value > system.void_points.max) { system.void_points.value = system.void_points.max; } } } /** * Set derived attributes (endurance, composure, focus, vigilance) from rings values * @param {Object} system */ static computeDerivedAttributes(system) { system.endurance = (Number(system.rings.earth) + Number(system.rings.fire)) * 2; system.composure = (Number(system.rings.earth) + Number(system.rings.water)) * 2; system.focus = Number(system.rings.air) + Number(system.rings.fire); system.vigilance = Math.ceil((Number(system.rings.air) + Number(system.rings.water)) / 2); } /** * Add a Ring/Skill point to the current actor if the item is a advancement * @param {Item} item * @return {Promise} */ async addBonus(item) { return this._updateActorFromAdvancement(item, true); } /** * Remove a Ring/Skill point to the current actor if the item is a advancement * @param {Item} item * @return {Promise} */ async removeBonus(item) { return this._updateActorFromAdvancement(item, false); } /** * Alter Actor skill/ring from a advancement * @param {Item} item * @param {boolean} isAdd True=add, false=remove * @return {Promise} * @private */ async _updateActorFromAdvancement(item, isAdd) { if (item && item.type === "advancement") { const actor = foundry.utils.duplicate(this.system); const itemData = item.system; if (itemData.advancement_type === "ring") { // Ring if (isAdd) { actor.rings[itemData.ring] = Math.min(9, actor.rings[itemData.ring] + 1); } else { actor.rings[itemData.ring] = Math.max(1, actor.rings[itemData.ring] - 1); } } else { // Skill const skillCatId = CONFIG.l5r5e.skills.get(itemData.skill); if (skillCatId) { if (isAdd) { actor.skills[skillCatId][itemData.skill] = Math.min( 9, actor.skills[skillCatId][itemData.skill] + 1 ); } else { actor.skills[skillCatId][itemData.skill] = Math.max( 0, actor.skills[skillCatId][itemData.skill] - 1 ); } } } // Update Actor await this.update({ system: foundry.utils.diffObject(this.system, actor), }); } } /** * Render the text template for this Actor (tooltips and chat) * @return {Promise} */ async renderTextTemplate() { const sheetData = (await this.sheet?.getData()) || this; const tpl = await renderTemplate(`${CONFIG.l5r5e.paths.templates}actors/actor-text.html`, sheetData); if (!tpl) { return null; } return tpl; } /** * Return true if this actor is a PC or NPC * @return {boolean} */ get isCharacter() { return ["character", "npc"].includes(this.type); } /** * Return true if a weapon is equipped * @return {boolean} */ get haveWeaponEquipped() { return this.items.some((e) => e.type === "weapon" && !!e.system.equipped); } /** * Return true if a weapon is readied * @return {boolean} */ get haveWeaponReadied() { return this.items.some((e) => e.type === "weapon" && !!e.system.equipped && !!e.system.readied); } /** * Return true if a armor is equipped * @return {boolean} */ get haveArmorEquipped() { return this.items.some((e) => e.type === "armor" && !!e.system.equipped); } /** * Return true if this actor is prepared (overridden by global) * @return {boolean} */ get isPrepared() { if (!this.isCharacter) { return false; } const cfg = { character: game.settings.get("l5r5e", "initiative-prepared-character"), adversary: game.settings.get("l5r5e", "initiative-prepared-adversary"), minion: game.settings.get("l5r5e", "initiative-prepared-minion"), }; // Prepared is a boolean or if null we get the info in the actor let isPrepared = this.type === "character" ? cfg.character : cfg[this.system.type]; if (isPrepared === "null") { isPrepared = this.system.prepared ? "true" : "false"; } return isPrepared; } /** * Return the Status Rank of this actor * @return {number|null} */ get statusRank() { if (!this.isCharacter) { return null; } return Math.floor(this.system.social.status / 10); } /** * Return the Intrigue Rank of this actor * @return {number|null} */ get intrigueRank() { if (!this.isCharacter) { return null; } return this.type === "npc" ? this.system.conflict_rank.social : this.system.identity.school_rank; } /** * Return the Martial Rank of this actor * @return {number|null} */ get martialRank() { if (!this.isCharacter) { return null; } return this.type === "npc" ? this.system.conflict_rank.martial : this.system.identity.school_rank; } }