ds4/src/module/item/item.ts

197 lines
7 KiB
TypeScript

// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
//
// SPDX-License-Identifier: MIT
import { DS4 } from "../config";
import { createCheckRoll } from "../rolls/check-factory";
import notifications from "../ui/notifications";
import { AttackType, ItemType } from "./item-data-source";
import { calculateSpellPrice } from "./type-specific-helpers/spell";
declare global {
interface DocumentClassConfig {
Item: typeof DS4Item;
}
}
/**
* The Item class for DS4
*/
export class DS4Item extends Item {
/** @override */
prepareData(): void {
super.prepareData();
}
/** @override */
prepareDerivedData(): void {
if (this.data.type === "talent") {
const data = this.data.data;
data.rank.total = data.rank.base + data.rank.mod;
}
if (this.data.type === "weapon" || this.data.type === "spell") {
this.data.data.rollable = this.data.data.equipped;
} else {
this.data.data.rollable = false;
}
if (this.data.type === "spell") {
this.data.data.price = calculateSpellPrice(this.data.data);
}
}
isNonEquippedEuipable(): boolean {
return "equipped" in this.data.data && !this.data.data.equipped;
}
/**
* The number of times that active effect changes originating from this item should be applied.
*/
get activeEffectFactor(): number | undefined {
if (this.data.type === "talent") {
return this.data.data.rank.total;
}
return 1;
}
/**
* The list of item types that are rollable.
*/
static get rollableItemTypes(): ItemType[] {
return ["weapon", "spell"];
}
/**
* Roll a check for an action with this item.
*/
async roll(): Promise<void> {
switch (this.data.type) {
case "weapon":
return this.rollWeapon();
case "spell":
return this.rollSpell();
default:
throw new Error(game.i18n.format("DS4.ErrorRollingForItemTypeNotPossible", { type: this.data.type }));
}
}
protected async rollWeapon(): Promise<void> {
if (!(this.data.type === "weapon")) {
throw new Error(
game.i18n.format("DS4.ErrorWrongItemType", {
actualType: this.data.type,
expectedType: "weapon",
id: this.id,
name: this.name,
}),
);
}
if (!this.data.data.equipped) {
return notifications.warn(
game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
name: this.name,
id: this.id,
type: this.data.type,
}),
);
}
if (!this.actor) {
throw new Error(game.i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id }));
}
const ownerDataData = this.actor.data.data;
const weaponBonus = this.data.data.weaponBonus;
const combatValue = await this.getCombatValueKeyForAttackType(this.data.data.attackType);
const checkTargetNumber = ownerDataData.combatValues[combatValue].total + weaponBonus;
await createCheckRoll(checkTargetNumber, {
rollMode: game.settings.get("core", "rollMode"),
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
flavor: game.i18n.format("DS4.ItemWeaponCheckFlavor", { actor: this.actor.name, weapon: this.name }),
});
}
protected async rollSpell(): Promise<void> {
if (!(this.data.type === "spell")) {
throw new Error(
game.i18n.format("DS4.ErrorWrongItemType", {
actualType: this.data.type,
expectedType: "spell",
id: this.id,
name: this.name,
}),
);
}
if (!this.data.data.equipped) {
return notifications.warn(
game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
name: this.name,
id: this.id,
type: this.data.type,
}),
);
}
if (!this.actor) {
throw new Error(game.i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id }));
}
const ownerDataData = this.actor.data.data;
const spellBonus = Number.isNumeric(this.data.data.bonus) ? parseInt(this.data.data.bonus) : undefined;
if (spellBonus === undefined) {
notifications.info(
game.i18n.format("DS4.InfoManuallyEnterSpellBonus", {
name: this.name,
spellBonus: this.data.data.bonus,
}),
);
}
const spellType = this.data.data.spellType;
const checkTargetNumber = ownerDataData.combatValues[spellType].total + (spellBonus ?? 0);
await createCheckRoll(checkTargetNumber, {
rollMode: game.settings.get("core", "rollMode"),
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
flavor: game.i18n.format("DS4.ItemSpellCheckFlavor", { actor: this.actor.name, spell: this.name }),
});
}
protected async getCombatValueKeyForAttackType(attackType: AttackType): Promise<"meleeAttack" | "rangedAttack"> {
if (attackType === "meleeRanged") {
const { melee, ranged } = { ...DS4.i18n.attackTypes };
const identifier = "attack-type-selection";
return Dialog.prompt({
title: game.i18n.localize("DS4.DialogAttackTypeSelection"),
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
selects: [
{
label: game.i18n.localize("DS4.AttackType"),
identifier,
options: { melee, ranged },
},
],
}),
label: game.i18n.localize("DS4.GenericOkButton"),
callback: (html) => {
const selectedAttackType = html.find(`#${identifier}`).val();
if (selectedAttackType !== "melee" && selectedAttackType !== "ranged") {
throw new Error(
game.i18n.format("DS4.ErrorUnexpectedAttackType", {
actualType: selectedAttackType,
expectedTypes: "'melee', 'ranged'",
}),
);
}
return `${selectedAttackType}Attack` as const;
},
});
} else {
return `${attackType}Attack` as const;
}
}
}