Files
l5rx-chiaroscuro/system/scripts/actor.js

427 lines
14 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(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`;
}
// Only for new actors (New actor has no items, duplicates does)
if (!docData.items) {
// Some tweak on actors prototypeToken
docData.prototypeToken = docData.prototypeToken || {};
switch (docData.type) {
case "character":
// Load skills from core compendiums (only for pc character)
docData.items = [];
await ActorL5r5e.addSkillsFromDefaultList(docData);
// Set token properties
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);
}
/**
* Add all the skills from compendiums to "items"
*/
static async addSkillsFromDefaultList(docData) {
console.log(`L5R5E | Adding default skills to ${docData.name}`);
const skillList = await game.l5r5e.HelpersL5r5e.getSkillsItemsList();
skillList.forEach(item => {
// Get the json data and replace the object id/rank
const tmpData = item.toObject();
tmpData._id = foundry.utils.randomID();
tmpData.system.rank = 0;
docData.items.push(tmpData);
});
}
/**
* 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.isNpc && 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(CONFIG.l5r5e.systemName, "gm-monitor-actors").find((e) => e === this.id)) {
game.l5r5e.HelpersL5r5e.refreshLocalAndSocket("l5r5e-gm-monitor");
}
});
}
/** @override */
prepareData() {
super.prepareData();
if (this.isCharacterType) {
const system = this.system;
// No automation for npc as they cheat in stats
if (this.isCharacter) {
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") {
return;
}
const actorSystem = foundry.utils.duplicate(this.system);
const itemData = item.system;
if (itemData.advancement_type === "ring") {
// Ring
if (isAdd) {
actorSystem.rings[itemData.ring] = Math.min(9, actorSystem.rings[itemData.ring] + 1);
} else {
actorSystem.rings[itemData.ring] = Math.max(1, actorSystem.rings[itemData.ring] - 1);
}
// Update Actor
await this.update({
system: foundry.utils.diffObject(this.system, actorSystem),
});
} else {
// Skill
let skillItem = await fromUuid(itemData.skill); // Skill itemUuid
if (!skillItem) {
console.warn("L5R5E | Unknown skill item uuid", itemData.skill);
return;
}
// Out of actor item ?
if (!skillItem.actor || skillItem.actor._id !== this._id) {
const checkItem = this.items.getName(skillItem.name);
if (checkItem) {
skillItem = checkItem;
} else {
throw new Error(`Unable to find "${skillItem.name}" on this actor`);
}
}
const newRank = isAdd
? Math.min(9, skillItem.system.rank + 1)
: Math.max(0, skillItem.system.rank - 1);
if (skillItem.system.rank === newRank) {
return;
}
// Update Item
await skillItem.update({ "system.rank": newRank });
}
}
/**
* 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 isCharacterType() {
return ["character", "npc"].includes(this.type);
}
/**
* Return true if this actor is a Character
* @return {boolean}
*/
get isCharacter() {
return this.type === "character";
}
/**
* Return true if this actor is a NPC
* @return {boolean}
*/
get isNpc() {
return this.type === "npc";
}
/**
* Return true if this actor is an Adversary
* @return {boolean}
*/
get isAdversary() {
return this.isNpc && this.system.type === "adversary";
}
/**
* Return true if this actor is a Minion
* @return {boolean}
*/
get isMinion() {
return this.isNpc && this.system.type === "minion";
}
/**
* Return true if this actor is an Army
* @return {boolean}
*/
get isArmy() {
return this.type === "army";
}
/**
* Return true if this actor have an active player as owner
* @returns {boolean}
*/
get hasPlayerOwnerActive() {
return game.users.find((u) => !!u.active && u.character?.id === this.id);
}
/**
* Return true if this actor can do a initiative roll
* @returns {boolean}
*/
get canDoInitiativeRoll() {
return game.combat?.combatants.some(
(c) => !c.initiative && (c.tokenId === this.token?._id || (!this.token && c.actorId === this._id))
);
}
/**
* 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.isCharacterType) {
return false;
}
const cfg = {
character: game.settings.get(CONFIG.l5r5e.systemName, "initiative-prepared-character"),
adversary: game.settings.get(CONFIG.l5r5e.systemName, "initiative-prepared-adversary"),
minion: game.settings.get(CONFIG.l5r5e.systemName, "initiative-prepared-minion"),
};
// Prepared is a boolean or if null we get the info in the actor
let isPrepared = this.isCharacter ? cfg.character : cfg[this.system.type];
if (isPrepared === "actor") {
isPrepared = this.system.prepared ? "true" : "false";
}
return isPrepared;
}
/**
* Return the Status Rank of this actor
* @return {number|null}
*/
get statusRank() {
if (!this.isCharacterType) {
return null;
}
return Math.floor(this.system.social.status / 10);
}
/**
* Return the Intrigue Rank of this actor
* @return {number|null}
*/
get intrigueRank() {
if (!this.isCharacterType) {
return null;
}
return this.isNpc ? this.system.conflict_rank.social : this.system.identity.school_rank;
}
/**
* Return the Martial Rank of this actor
* @return {number|null}
*/
get martialRank() {
if (!this.isCharacterType) {
return null;
}
return this.isNpc ? this.system.conflict_rank.martial : this.system.identity.school_rank;
}
}