# Conflicts: # CHANGELOG.md # system/lang/en-en.json # system/lang/fr-fr.json # system/scripts/actor.js # system/scripts/actors/base-character-sheet.js # system/scripts/combat.js # system/scripts/config.js # system/scripts/dice/dice-picker-dialog.js # system/scripts/dice/roll-n-keep-dialog.js # system/scripts/gm/gm-monitor.js # system/scripts/gm/gm-toolbox.js # system/scripts/hooks.js # system/scripts/items/technique-sheet.js # system/scripts/main-l5r5e.js # system/scripts/migration.js # system/scripts/preloadTemplates.js # system/scripts/settings.js # system/scripts/socket-handler.js # system/styles/l5r5e.css # system/system.json # system/templates/actors/character-sheet.html
434 lines
14 KiB
JavaScript
434 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.namespace, "gm-monitor-actors").some((uuid) => uuid === this.uuid)) {
|
|
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);
|
|
|
|
// Modifiers from conditions
|
|
const modifiers = system.modifiers?.character;
|
|
system.endurance = system.endurance + (Number(modifiers?.endurance) || 0);
|
|
system.composure = system.composure + (Number(modifiers?.composure) || 0);
|
|
system.focus = system.focus + (Number(modifiers?.focus) || 0);
|
|
system.vigilance = system.vigilance + (Number(modifiers?.vigilance) || 0);
|
|
}
|
|
|
|
/**
|
|
* 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.namespace, "initiative-prepared-character"),
|
|
adversary: game.settings.get(CONFIG.l5r5e.namespace, "initiative-prepared-adversary"),
|
|
minion: game.settings.get(CONFIG.l5r5e.namespace, "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;
|
|
}
|
|
}
|