2021-03-04 00:14:16 +01:00
|
|
|
import { DS4Actor } from "../actor/actor";
|
|
|
|
import { DS4 } from "../config";
|
|
|
|
import { createCheckRoll } from "../rolls/check-factory";
|
2021-03-04 01:54:51 +01:00
|
|
|
import notifications from "../ui/notifications";
|
2021-03-04 00:14:16 +01:00
|
|
|
import { AttackType, DS4ItemData } from "./item-data";
|
2020-12-23 18:23:26 +01:00
|
|
|
|
2020-12-23 16:52:20 +01:00
|
|
|
/**
|
2021-02-07 11:51:36 +01:00
|
|
|
* The Item class for DS4
|
2020-12-23 16:52:20 +01:00
|
|
|
*/
|
2021-01-26 03:55:18 +01:00
|
|
|
export class DS4Item extends Item<DS4ItemData> {
|
2020-12-23 16:52:20 +01:00
|
|
|
/**
|
2021-02-07 11:51:36 +01:00
|
|
|
* @override
|
2020-12-23 16:52:20 +01:00
|
|
|
*/
|
2020-12-23 18:23:26 +01:00
|
|
|
prepareData(): void {
|
2020-12-23 16:52:20 +01:00
|
|
|
super.prepareData();
|
2021-01-06 01:24:37 +01:00
|
|
|
this.prepareDerivedData();
|
2020-12-23 16:52:20 +01:00
|
|
|
}
|
2021-01-06 01:24:37 +01:00
|
|
|
|
|
|
|
prepareDerivedData(): void {
|
2021-02-05 03:42:42 +01:00
|
|
|
if (this.data.type === "talent") {
|
|
|
|
const data = this.data.data;
|
2021-01-06 11:52:11 +01:00
|
|
|
data.rank.total = data.rank.base + data.rank.mod;
|
2021-01-06 01:24:37 +01:00
|
|
|
}
|
2021-03-04 01:54:51 +01:00
|
|
|
if (this.data.type === "weapon" || this.data.type === "spell") {
|
|
|
|
this.data.data.rollable = this.data.data.equipped;
|
2021-03-04 00:14:16 +01:00
|
|
|
}
|
2021-01-06 01:24:37 +01:00
|
|
|
}
|
2021-02-16 03:26:26 +01:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
2021-02-18 13:36:36 +01:00
|
|
|
get activeEffectFactor(): number | undefined {
|
2021-02-16 03:26:26 +01:00
|
|
|
if (this.data.type === "talent") {
|
2021-02-18 13:36:36 +01:00
|
|
|
return this.data.data.rank.total;
|
2021-02-16 03:26:26 +01:00
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
}
|
2021-03-04 00:14:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Roll a check for a action with this item.
|
|
|
|
*/
|
|
|
|
async roll(): Promise<void> {
|
|
|
|
if (!this.isOwnedItem()) {
|
|
|
|
throw new Error(game.i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id }));
|
|
|
|
}
|
2021-03-04 01:54:51 +01:00
|
|
|
|
|
|
|
switch (this.data.type) {
|
|
|
|
case "weapon":
|
|
|
|
await this.rollWeapon();
|
|
|
|
case "spell":
|
|
|
|
await this.rollSpell();
|
|
|
|
default:
|
|
|
|
throw new Error(game.i18n.format("DS4.ErrorRollingForItemTypeNotPossible", { type: this.data.type }));
|
2021-03-04 00:14:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async rollWeapon(this: this & { readonly isOwned: true }): 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,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-04 01:54:51 +01:00
|
|
|
if (!this.data.data.equipped) {
|
|
|
|
throw new Error(
|
|
|
|
game.i18n.format("DS4.ErrorItemMustBeEquippedToBeRolled", {
|
|
|
|
name: this.name,
|
|
|
|
id: this.id,
|
|
|
|
type: this.data.type,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-04 00:41:57 +01:00
|
|
|
const ownerDataData = ((this.actor as unknown) as DS4Actor).data.data; // TODO(types): Improve so that the concrete Actor type is known here
|
2021-03-04 00:14:16 +01:00
|
|
|
const weaponBonus = this.data.data.weaponBonus;
|
|
|
|
const combatValue = await this.getCombatValueKeyForAttackType(this.data.data.attackType);
|
2021-03-04 00:41:57 +01:00
|
|
|
const checkTargetValue = (ownerDataData.combatValues[combatValue].total as number) + weaponBonus;
|
|
|
|
await createCheckRoll(checkTargetValue, {
|
|
|
|
rollMode: game.settings.get("core", "rollMode"),
|
2021-03-04 01:54:51 +01:00
|
|
|
maxCritSuccess: ownerDataData.rolling.maximumCoupResult,
|
|
|
|
minCritFailure: ownerDataData.rolling.minimumFumbleResult,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private 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) {
|
|
|
|
throw new Error(
|
|
|
|
game.i18n.format("DS4.ErrorItemMustBeEquippedToBeRolled", {
|
|
|
|
name: this.name,
|
|
|
|
id: this.id,
|
|
|
|
type: this.data.type,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const ownerDataData = ((this.actor as unknown) as DS4Actor).data.data; // TODO(types): Improve so that the concrete Actor type is known here
|
|
|
|
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 checkTargetValue = (ownerDataData.combatValues[spellType].total as number) + (spellBonus ?? 0);
|
|
|
|
|
|
|
|
await createCheckRoll(checkTargetValue, {
|
|
|
|
rollMode: game.settings.get("core", "rollMode"),
|
2021-03-04 00:41:57 +01:00
|
|
|
maxCritSuccess: ownerDataData.rolling.maximumCoupResult,
|
|
|
|
minCritFailure: ownerDataData.rolling.minimumFumbleResult,
|
|
|
|
});
|
2021-03-04 00:14:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private async getCombatValueKeyForAttackType(attackType: AttackType): Promise<"meleeAttack" | "rangedAttack"> {
|
|
|
|
if (attackType === "meleeRanged") {
|
|
|
|
const { melee, ranged } = { ...DS4.i18n.attackTypes };
|
|
|
|
const identifier = "attack-type-selection";
|
|
|
|
const label = game.i18n.localize("DS4.AttackType");
|
|
|
|
const answer = Dialog.prompt({
|
|
|
|
title: game.i18n.localize("DS4.AttackTypeSelection"),
|
|
|
|
content: await renderTemplate("systems/ds4/templates/common/simple-select-form.hbs", {
|
|
|
|
label,
|
|
|
|
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;
|
|
|
|
},
|
|
|
|
render: () => undefined, // TODO(types): This is actually optional, remove when types are updated )
|
|
|
|
options: { jQuery: true },
|
|
|
|
});
|
|
|
|
return answer;
|
|
|
|
} else {
|
|
|
|
return `${attackType}Attack` as const;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Type-guarding variant to check if the item is owned.
|
|
|
|
*/
|
|
|
|
isOwnedItem(): this is this & { readonly isOwned: true } {
|
|
|
|
return this.isOwned;
|
|
|
|
}
|
2020-12-23 16:52:20 +01:00
|
|
|
}
|