import { DS4Actor } from "../actor/actor"; import { DS4 } from "../config"; import { createCheckRoll } from "../rolls/check-factory"; import notifications from "../ui/notifications"; import { AttackType, DS4ItemData, ItemType } from "./item-data"; import { DS4ItemPreparedData } from "./item-prepared-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; } else { this.data.data.rollable = false; } } 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 { if (!this.isOwnedItem()) { throw new Error(game.i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id })); } 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(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) { return notifications.warn( game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", { name: this.name, id: this.id, type: this.data.type, }), ); } const actor = (this.actor as unknown) as DS4Actor; // TODO(types): Improve so that the concrete Actor type is known here const ownerDataData = 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") as Const.DiceRollMode, // TODO(types): Type this setting in upstream maximumCoupResult: ownerDataData.rolling.maximumCoupResult, minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult, flavor: game.i18n.format("DS4.ItemWeaponCheckFlavor", { actor: actor.name, weapon: this.name }), }); } protected 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) { return notifications.warn( game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", { name: this.name, id: this.id, type: this.data.type, }), ); } const actor = (this.actor as unknown) as DS4Actor; // TODO(types): Improve so that the concrete Actor type is known here const ownerDataData = 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") as Const.DiceRollMode, // TODO(types): Type this setting in upstream maximumCoupResult: ownerDataData.rolling.maximumCoupResult, minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult, flavor: game.i18n.format("DS4.ItemSpellCheckFlavor", { actor: 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"; 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; }, 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; } }