foundryacks/src/module/actor/entity.js

893 lines
22 KiB
JavaScript

import { AcksDice } from "../dice.js";
export class AcksActor extends Actor {
/**
* Extends data from base Actor class
*/
prepareData() {
super.prepareData();
const data = this.data.data;
// Compute modifiers from actor scores
this.computeModifiers();
this._isSlow();
this.computeAC();
this.computeEncumbrance();
this.computeTreasure();
this.computeBHR();
this.computeAAB();
// Determine Initiative
if (game.settings.get("acks", "initiative") != "group") {
data.initiative.value = data.initiative.mod;
if (this.data.type == "character") {
data.initiative.value += data.scores.dex.mod;
if (data.isSlow) {
data.initiative.value -= 1;
}
}
} else {
data.initiative.value = 0;
}
data.movement.encounter = data.movement.base / 3;
}
/* -------------------------------------------- */
/* Socket Listeners and Handlers
/* -------------------------------------------- */
async getExperience(value, options = {}) {
if (this.data.type != "character") {
return;
}
let modified = Math.floor(
value + (this.data.data.details.xp.bonus * value) / 100
);
await this.update({
"data.details.xp.value": modified + this.data.data.details.xp.value,
});
const speaker = ChatMessage.getSpeaker({ actor: this });
await ChatMessage.create({
content: game.i18n.format("ACKS.messages.GetExperience", {
name: this.name,
value: modified,
}),
speaker,
});
}
isNew() {
const data = this.data.data;
if (this.data.type == "character") {
let ct = 0;
Object.values(data.scores).forEach((el) => {
ct += el.value;
});
return ct == 0 ? true : false;
} else if (this.data.type == "monster") {
let ct = 0;
Object.values(data.saves).forEach((el) => {
ct += el.value;
});
return ct == 0 ? true : false;
}
}
async generateSave(hd) {
let saves = {};
for (let i = 0; i <= hd; i++) {
let tmp = CONFIG.ACKS.monster_saves[i];
if (tmp) {
saves = tmp;
}
}
await this.update({
"data.saves": {
death: {
value: saves.d,
},
wand: {
value: saves.w,
},
paralysis: {
value: saves.p,
},
breath: {
value: saves.b,
},
spell: {
value: saves.s,
},
},
});
}
/* -------------------------------------------- */
/* Rolls */
/* -------------------------------------------- */
async rollHP(options = {}) {
let roll = new Roll(this.data.data.hp.hd);
await roll.evaluate({
async: true,
});
await this.update({
data: {
hp: {
max: roll.total,
value: roll.total,
},
},
});
}
rollSave(save, options = {}) {
const label = game.i18n.localize(`ACKS.saves.${save}.long`);
const rollParts = ["1d20"];
if (this.data.type == "character") {
rollParts.push(this.data.data.save.mod);
}
let data = {};
if (this.data.type == "character") {
data = {
actor: this.data,
roll: {
type: "above",
target: this.data.data.saves[save].value,
magic: this.data.data.scores.wis.mod
},
details: game.i18n.format("ACKS.roll.details.save", { save: label }),
};
} else if (this.data.type == "monster") {
data = {
actor: this.data,
roll: {
type: "above",
target: this.data.data.saves[save].value,
},
details: game.i18n.format("ACKS.roll.details.save", { save: label }),
};
}
let skip = options.event && options.event.ctrlKey;
const rollMethod = this.data.type == "character" ? AcksDice.RollSave : AcksDice.Roll;
// Roll and return
return rollMethod({
event: options.event,
parts: rollParts,
data: data,
skipDialog: skip,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: game.i18n.format("ACKS.roll.save", { save: label }),
title: game.i18n.format("ACKS.roll.save", { save: label }),
});
}
rollMorale(options = {}) {
const rollParts = ["2d6"];
rollParts.push(this.data.data.details.morale);
const data = {
actor: this.data,
roll: {
type: "table",
table: {
1: game.i18n.format("ACKS.morale.retreat", {
name: this.data.name,
}),
3: game.i18n.format("ACKS.morale.fightingWithdrawal", {
name: this.data.name,
}),
6: game.i18n.format("ACKS.morale.fight", {
name: this.data.name,
}),
9: game.i18n.format("ACKS.morale.advanceAndPursue", {
name: this.data.name,
}),
12: game.i18n.format("ACKS.morale.fightToTheDeath", {
name: this.data.name,
}),
},
},
};
let skip = options.event && options.event.ctrlKey;
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: data,
skipDialog: skip,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: game.i18n.localize("ACKS.roll.morale"),
title: game.i18n.localize("ACKS.roll.morale"),
});
}
rollLoyalty(options = {}) {
const rollParts = ["2d6"];
rollParts.push(this.data.data.details.morale);
const data = {
actor: this.data,
roll: {
type: "table",
table: {
1: game.i18n.format("ACKS.loyalty.hostility", {
name: this.data.name,
}),
3: game.i18n.format("ACKS.loyalty.resignation", {
name: this.data.name,
}),
6: game.i18n.format("ACKS.loyalty.grudging", {
name: this.data.name,
}),
9: game.i18n.format("ACKS.loyalty.loyal", {
name: this.data.name,
}),
12: game.i18n.format("ACKS.loyalty.fanatic", {
name: this.data.name,
}),
},
},
};
let skip = options.event && options.event.ctrlKey;
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: data,
skipDialog: skip,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: game.i18n.localize("ACKS.loyalty.check"),
title: game.i18n.localize("ACKS.loyalty.check"),
});
}
rollReaction(options = {}) {
const rollParts = ["2d6"];
const data = {
actor: this.data,
roll: {
type: "table",
table: {
2: game.i18n.format("ACKS.reaction.Hostile", {
name: this.data.name,
}),
3: game.i18n.format("ACKS.reaction.Unfriendly", {
name: this.data.name,
}),
6: game.i18n.format("ACKS.reaction.Neutral", {
name: this.data.name,
}),
9: game.i18n.format("ACKS.reaction.Indifferent", {
name: this.data.name,
}),
12: game.i18n.format("ACKS.reaction.Friendly", {
name: this.data.name,
}),
},
},
};
let skip = options.event && options.event.ctrlKey;
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: data,
skipDialog: skip,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: game.i18n.localize("ACKS.reaction.check"),
title: game.i18n.localize("ACKS.reaction.check"),
});
}
rollCheck(score, options = {}) {
const label = game.i18n.localize(`ACKS.scores.${score}.long`);
const rollParts = ["1d20"];
const data = {
actor: this.data,
roll: {
type: "check",
target: this.data.data.scores[score].value,
},
details: game.i18n.format("ACKS.roll.details.attribute", {
score: label,
}),
};
let skip = options.event && options.event.ctrlKey;
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: data,
skipDialog: skip,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: game.i18n.format("ACKS.roll.attribute", { attribute: label }),
title: game.i18n.format("ACKS.roll.attribute", { attribute: label }),
});
}
rollHitDice(options = {}) {
const label = game.i18n.localize(`ACKS.roll.hd`);
const rollParts = [this.data.data.hp.hd];
if (this.data.type == "character") {
rollParts.push(this.data.data.scores.con.mod);
}
const data = {
actor: this.data,
roll: {
type: "hitdice",
},
};
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: data,
skipDialog: true,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: label,
title: label,
});
}
rollBHR(options = {}) {
const label = game.i18n.localize(`ACKS.roll.bhr`);
const rollParts = [this.data.data.hp.bhr];
if (this.data.type == "character") {
rollParts.push();
}
const data = {
actor: this.data,
roll: {
type: "Healing",
},
};
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: data,
skipDialog: true,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: label,
title: label,
});
}
rollAppearing(options = {}) {
const rollParts = [];
let label = "";
if (options.check == "wilderness") {
rollParts.push(this.data.data.details.appearing.w);
label = "(2)";
} else {
rollParts.push(this.data.data.details.appearing.d);
label = "(1)";
}
const data = {
actor: this.data,
roll: {
type: {
type: "appearing",
},
},
};
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: data,
skipDialog: true,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: game.i18n.format("ACKS.roll.appearing", { type: label }),
title: game.i18n.format("ACKS.roll.appearing", { type: label }),
});
}
rollExploration(expl, options = {}) {
const label = game.i18n.localize(`ACKS.exploration.${expl}.long`);
const rollParts = ["1d20"];
const data = {
actor: this.data,
roll: {
type: "above",
target: this.data.data.exploration[expl],
},
details: game.i18n.format("ACKS.roll.details.exploration", {
expl: label,
}),
};
let skip = options.event && options.event.ctrlKey;
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: data,
skipDialog: skip,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: game.i18n.format("ACKS.roll.exploration", { exploration: label }),
title: game.i18n.format("ACKS.roll.exploration", { exploration: label }),
});
}
rollDamage(attData, options = {}) {
const data = this.data.data;
const rollData = {
actor: this.data,
item: attData.item,
roll: {
type: "damage",
},
};
let dmgParts = [];
if (!attData.roll.dmg) {
dmgParts.push("1d6");
} else {
dmgParts.push(attData.roll.dmg);
}
// Add Str to damage
if (attData.roll.type == "melee") {
dmgParts.push(data.scores.str.mod);
}
// Add Melee mod to damage
if (attData.roll.type == "melee") {
dmgParts.push(data.damage.mod.melee);
}
// Add Missile mod to damage
if (attData.roll.type == "missile") {
dmgParts.push(data.damage.mod.missile);
}
// Damage roll
AcksDice.Roll({
event: options.event,
parts: dmgParts,
data: rollData,
skipDialog: true,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: `${attData.label} - ${game.i18n.localize("ACKS.Damage")}`,
title: `${attData.label} - ${game.i18n.localize("ACKS.Damage")}`,
});
}
async targetAttack(data, type, options) {
if (game.user.targets.size > 0) {
for (let t of game.user.targets.values()) {
data.roll.target = t;
await this.rollAttack(data, {
type: type,
skipDialog: options.skipDialog,
});
}
} else {
this.rollAttack(data, { type: type, skipDialog: options.skipDialog });
}
}
rollAttack(attData, options = {}) {
const data = this.data.data;
let rollParts = ["1d20"];
if (game.settings.get("acks", "exploding20s")) {
rollParts = ["1d20x="];
}
const dmgParts = [];
let label = game.i18n.format("ACKS.roll.attacks", {
name: this.data.name,
});
if (!attData.item) {
dmgParts.push("1d6");
} else {
label = game.i18n.format("ACKS.roll.attacksWith", {
name: attData.item.name,
});
dmgParts.push(attData.item.data.damage);
}
let ascending = game.settings.get("acks", "ascendingAC");
if (ascending) {
rollParts.push(data.thac0.bba.toString());
}
if (options.type == "missile") {
rollParts.push(
data.scores.dex.mod.toString(),
data.thac0.mod.missile.toString()
);
} else if (options.type == "melee") {
rollParts.push(
data.scores.str.mod.toString(),
data.thac0.mod.melee.toString()
);
}
if (attData.item && attData.item.data.bonus) {
rollParts.push(attData.item.data.bonus);
}
let thac0 = data.thac0.value;
if (options.type == "melee") {
dmgParts.push(data.scores.str.mod);
}
// Add Melee mod to damage
if (options.type == "melee") {
dmgParts.push(data.damage.mod.melee);
}
// Add Missile mod to damage
if (options.type == "missile") {
dmgParts.push(data.damage.mod.missile);
}
const rollData = {
actor: this.data,
item: attData.item,
roll: {
type: options.type,
thac0: thac0,
dmg: dmgParts,
save: attData.roll.save,
target: attData.roll.target,
},
};
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: rollData,
skipDialog: options.skipDialog,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: label,
title: label,
});
}
async applyDamage(amount = 0, multiplier = 1) {
amount = Math.ceil(parseInt(amount) * multiplier);
const hp = this.data.data.hp;
// Remaining goes to health
const dh = Math.clamped(hp.value - amount, -99, hp.max);
// Update the Actor
await this.update({
"data.hp.value": dh,
});
}
static _valueFromTable(table, val) {
let output;
for (let i = 0; i <= val; i++) {
if (table[i] != undefined) {
output = table[i];
}
}
return output;
}
_isSlow() {
this.data.data.isSlow = false;
if (this.data.type != "character") {
return;
}
this.data.items.forEach((item) => {
if (item.type == "weapon" && item.data.slow && item.data.equipped) {
this.data.data.isSlow = true;
return;
}
});
}
computeEncumbrance() {
if (this.data.type != "character") {
return;
}
const data = this.data.data;
let option = game.settings.get("acks", "encumbranceOption");
// Compute encumbrance
let totalWeight = 0;
let hasItems = false;
Object.values(this.data.items).forEach((item) => {
if (item.type == "item" && !item.data.treasure) {
if (option === "detailed") totalWeight += 166.6;
// hasItems = true;
}
if (
item.type == "item" &&
(["complete", "disabled"].includes(option) || item.data.treasure)
) {
totalWeight += item.data.quantity.value * item.data.weight;
} else if (option != "basic" && ["weapon", "armor"].includes(item.type)) {
totalWeight += item.data.weight;
}
});
// if (option === "detailed" && hasItems) totalWeight += 166.6;
data.encumbrance = {
pct: Math.clamped(
// To correct for percentage bar not lining up with movement rates.
// (100 * parseFloat(totalWeight)) / data.encumbrance.max,
(100 * parseFloat(totalWeight)) / 20000,
0,
100
),
max: data.encumbrance.max,
encumbered: totalWeight > data.encumbrance.max,
value: Math.round(totalWeight),
};
if (data.config.movementAuto && option != "disabled") {
this._calculateMovement();
}
}
_calculateMovement() {
const data = this.data.data;
let option = game.settings.get("acks", "encumbranceOption");
let weight = data.encumbrance.value;
if (["detailed", "complete"].includes(option)) {
if (weight > data.encumbrance.max) {
data.movement.base = 0;
} else if (weight > 10000) {
data.movement.base = 30;
} else if (weight > 7000) {
data.movement.base = 60;
} else if (weight > 5000) {
data.movement.base = 90;
} else {
data.movement.base = 120;
}
} else if (option == "basic") {
const armors = this.data.items.filter((i) => i.type == "armor");
let heaviest = 0;
armors.forEach((a) => {
if (a.data.equipped) {
if (a.data.type == "light" && heaviest == 0) {
heaviest = 1;
} else if (a.data.type == "heavy") {
heaviest = 2;
}
}
});
switch (heaviest) {
case 0:
data.movement.base = 120;
break;
case 1:
data.movement.base = 90;
break;
case 2:
data.movement.base = 60;
break;
}
if (weight > game.settings.get("acks", "significantTreasure")) {
data.movement.base -= 30;
}
}
}
computeTreasure() {
if (this.data.type != "character") {
return;
}
const data = this.data.data;
// Compute treasure
let total = 0;
let treasure = this.data.items.filter(
(i) => i.data.type == "item" && i.data.data.treasure
);
treasure.forEach((item) => {
total += item.data.data.quantity.value * item.data.data.cost
});
data.treasure = total;
}
computeAC() {
if (this.data.type != "character") {
return;
}
// Compute AC
let baseAc = 9;
let baseAac = 0;
let AcShield = 0;
let AacShield = 0;
const data = this.data.data;
data.aac.naked = baseAac + data.scores.dex.mod;
data.ac.naked = baseAc - data.scores.dex.mod;
const armors = this.data.items.filter((i) => i.data.type == "armor");
armors.forEach((a) => {
if (a.data.data.equipped && a.data.type != "shield") {
baseAc = a.data.data.ac;
baseAac = a.data.data.aac.value;
} else if (a.data.data.equipped && a.data.type == "shield") {
AcShield = a.data.data.ac;
AacShield = a.data.data.aac.value;
}
});
data.aac.value = baseAac + data.scores.dex.mod + AacShield + data.aac.mod;
data.ac.value = baseAc - data.scores.dex.mod - AcShield - data.ac.mod;
data.ac.shield = AcShield;
data.aac.shield = AacShield;
}
computeModifiers() {
if (this.data.type != "character") {
return;
}
const data = this.data.data;
const standard = {
0: -3,
3: -3,
4: -2,
6: -1,
9: 0,
13: 1,
16: 2,
18: 3,
19: 4,
20: 5,
21: 6,
22: 7,
23: 8,
24: 9,
25: 10
};
data.scores.str.mod = AcksActor._valueFromTable(
standard,
data.scores.str.value
);
data.scores.int.mod = AcksActor._valueFromTable(
standard,
data.scores.int.value
);
data.scores.dex.mod = AcksActor._valueFromTable(
standard,
data.scores.dex.value
);
data.scores.cha.mod = AcksActor._valueFromTable(
standard,
data.scores.cha.value
);
data.scores.wis.mod = AcksActor._valueFromTable(
standard,
data.scores.wis.value
);
data.scores.con.mod = AcksActor._valueFromTable(
standard,
data.scores.con.value
);
const capped = {
0: -2,
3: -2,
4: -1,
6: -1,
9: 0,
13: 1,
16: 1,
18: 2,
};
data.scores.dex.init = AcksActor._valueFromTable(
standard,
data.scores.dex.value
);
data.scores.cha.npc = AcksActor._valueFromTable(
standard,
data.scores.cha.value
);
data.scores.cha.retain = data.scores.cha.mod + 4;
data.scores.cha.loyalty = data.scores.cha.mod;
const od = {
0: 0,
3: 30,
4: 26,
6: 22,
9: 18,
13: 14,
16: 10,
18: 6,
19: 2,
};
data.exploration.odMod = AcksActor._valueFromTable(
od,
data.scores.str.value
);
const literacy = {
3: "ACKS.Illiterate",
9: "ACKS.Literate",
};
data.languages.literacy = AcksActor._valueFromTable(
literacy,
data.scores.int.value
);
const spoken = {
0: "ACKS.NativeBroken",
3: "ACKS.Native",
13: "ACKS.NativePlus1",
16: "ACKS.NativePlus2",
18: "ACKS.NativePlus3",
19: "ACKS.NativePlus4",
20: "ACKS.NativePlus5",
21: "ACKS.NativePlus6",
22: "ACKS.NativePlus7",
23: "ACKS.NativePlus8",
24: "ACKS.NativePlus9",
25: "ACKS.NativePlus10",
};
data.languages.spoken = AcksActor._valueFromTable(
spoken,
data.scores.int.value
);
}
computeBHR() {
if (this.data.type != "character") {
return;
}
const data = this.data.data;
const bhrcalc = {
0: "1d2",
4: "1d3",
10: "1d4",
17: "1d6",
24: "1d8",
30: "1d10",
37: "2d6",
50: "2d8",
64: "2d10",
77: "2d12",
90: "3d10",
111: "4d10",
141: "5d10",
171: "6d10",
200: "7d10",
};
data.hp.bhr = AcksActor._valueFromTable(
bhrcalc,
data.hp.max
);
};
computeAAB() {
const data = this.data.data;
data.thac0.bba = 10 - data.thac0.throw;
}
}