Files
l5rx-chiaroscuro/system/scripts/actor.js
2022-07-21 16:08:47 +02:00

330 lines
11 KiB
JavaScript

/**
* 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(data, options = {}) {
// if (!Object.keys(data).includes("type")) {
// data.type = "character";
// }
// Replace default image
if (data.img === undefined) {
data.img = `${CONFIG.l5r5e.paths.assets}icons/actors/${data.type}.svg`;
}
// Some tweak on actors prototypeToken
data.prototypeToken = data.prototypeToken || {};
switch (data.type) {
case "character":
foundry.utils.mergeObject(
data.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(
data.prototypeToken,
{
actorLink: true,
disposition: 0, // neutral
bar1: {
attribute: "fatigue",
},
bar2: {
attribute: "strife",
},
},
{ overwrite: false }
);
break;
case "army":
foundry.utils.mergeObject(
data.prototypeToken,
{
actorLink: true,
disposition: 0, // neutral
bar1: {
attribute: "battle_readiness.casualties_strength",
},
bar2: {
attribute: "battle_readiness.panic_discipline",
},
},
{ overwrite: false }
);
break;
}
await super.create(data, options);
}
/**
* Entity-specific actions that should occur when the Entity is updated
* @override
*/
async update(data = {}, context = {}) {
// fix foundry v0.8.8 (config token=object, update=flat array)
data = foundry.utils.flattenObject(data);
// Need a _id
if (!data["_id"]) {
data["_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 (!!data["system.type"] && this.type === "npc" && data["system.type"] !== this.system.type) {
data["prototypeToken.actorLink"] = data["system.type"] === "adversary";
}
// Only on linked Actor
if (
!!data["prototypeToken.actorLink"] ||
(data["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 (
data[dataProp] &&
!data["prototypeToken." + TknProp] &&
this[dataProp] === foundry.utils.getProperty(this.prototypeToken, TknProp) &&
this[dataProp] !== data[dataProp]
) {
data["prototypeToken." + TknProp] = data[dataProp];
}
});
}
// Now using updateDocuments
return Actor.updateDocuments([data], 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<void>}
*/
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<void>}
*/
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<void>}
* @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<string|null>}
*/
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;
}
}