import { DS4Actor } from "../actor/actor"; import { DS4 } from "../config"; import { createCheckRoll } from "../rolls/check-factory"; import notifications from "../ui/notifications"; import { AttackType, DS4ItemData } from "./item-data"; /** * The Item class for DS4 */ export class DS4Item extends Item { /** * @override */ prepareData(): void { super.prepareData(); this.prepareDerivedData(); } 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; } } 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; } /** * Roll a check for a action with this item. */ async roll(): Promise { if (!this.isOwnedItem()) { throw new Error(game.i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id })); } 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 })); } } private async rollWeapon(this: this & { readonly isOwned: true }): Promise { 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) { 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 weaponBonus = this.data.data.weaponBonus; const combatValue = await this.getCombatValueKeyForAttackType(this.data.data.attackType); const checkTargetValue = (ownerDataData.combatValues[combatValue].total as number) + weaponBonus; await createCheckRoll(checkTargetValue, { rollMode: game.settings.get("core", "rollMode"), maxCritSuccess: ownerDataData.rolling.maximumCoupResult, minCritFailure: ownerDataData.rolling.minimumFumbleResult, }); } private async rollSpell(): Promise { 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"), maxCritSuccess: ownerDataData.rolling.maximumCoupResult, minCritFailure: ownerDataData.rolling.minimumFumbleResult, }); } 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; } }