diff --git a/lang/en.json b/lang/en.json index 1512f7d36d..5ea3e0cec1 100644 --- a/lang/en.json +++ b/lang/en.json @@ -547,6 +547,7 @@ "Label": "Attack Mode", "Offhand": "Offhand", "OneHanded": "One-Handed", + "Ranged": "Ranged", "Thrown": "Thrown", "ThrownOffhand": "Offhand Throw", "TwoHanded": "Two-Handed" diff --git a/module/applications/activity/attack-sheet.mjs b/module/applications/activity/attack-sheet.mjs index a7bc046429..99eb73f241 100644 --- a/module/applications/activity/attack-sheet.mjs +++ b/module/applications/activity/attack-sheet.mjs @@ -77,10 +77,12 @@ export default class AttackSheet extends ActivitySheet { context.attackTypeOptions = Object.entries(CONFIG.DND5E.attackTypes) .map(([value, config]) => ({ value, label: config.label })); - if ( this.item.system.attackType ) context.attackTypeOptions.unshift({ + if ( this.item.system.validAttackTypes?.size ) context.attackTypeOptions.unshift({ value: "", label: game.i18n.format("DND5E.DefaultSpecific", { - default: CONFIG.DND5E.attackTypes[this.item.system.attackType].label.toLowerCase() + default: game.i18n.getListFormatter({ type: "disjunction" }).format( + Array.from(this.item.system.validAttackTypes).map(t => CONFIG.DND5E.attackTypes[t].label.toLowerCase()) + ) }) }); diff --git a/module/config.mjs b/module/config.mjs index 2087869374..2160e3e0c9 100644 --- a/module/config.mjs +++ b/module/config.mjs @@ -2833,6 +2833,9 @@ DND5E.attackModes = Object.seal({ offhand: { label: "DND5E.ATTACK.Mode.Offhand" }, + ranged: { + label: "DND5E.ATTACK.Mode.Ranged" + }, thrown: { label: "DND5E.ATTACK.Mode.Thrown" }, diff --git a/module/data/activity/attack-data.mjs b/module/data/activity/attack-data.mjs index e2830cd728..f62f89f5e8 100644 --- a/module/data/activity/attack-data.mjs +++ b/module/data/activity/attack-data.mjs @@ -139,14 +139,7 @@ export default class AttackActivityData extends BaseActivityData { get validAttackTypes() { const sourceType = this._source.attack.type.value; if ( sourceType ) return new Set([sourceType]); - if ( this.item.type !== "weapon" ) return new Set(); - - const types = new Set(); - const attackType = this.attack.type.value || this.item.system.attackType; - if ( attackType === "melee" ) types.add("melee"); - if ( (attackType === "ranged") || ((this.item.system.attackType === "melee") - && this.item.system.properties.has("thr")) ) types.add("ranged"); - return types; + return this.item.system.validAttackTypes ?? new Set(); } /* -------------------------------------------- */ @@ -236,8 +229,7 @@ export default class AttackActivityData extends BaseActivityData { const key = attackMode.split("-").map(s => s.capitalize()).join(""); attackModeLabel = game.i18n.localize(`DND5E.ATTACK.Mode.${key}`); } - let actionType = this.actionType; - if ( (actionType === "mwak") && (attackMode?.startsWith("thrown")) ) actionType = "rwak"; + const actionType = this.getActionType(attackMode); let actionTypeLabel = game.i18n.localize(`DND5E.Action${actionType.toUpperCase()}`); const isLegacy = game.settings.get("dnd5e", "rulesVersion") === "legacy"; const isUnarmed = this.attack.type.classification === "unarmed"; @@ -267,9 +259,6 @@ export default class AttackActivityData extends BaseActivityData { const rollData = this.getRollData(); if ( this.attack.flat ) return CONFIG.Dice.BasicRoll.constructParts({ toHit: this.attack.bonus }, rollData); - let actionType = this.actionType; - if ( (actionType === "mwak") && attackMode?.startsWith("thrown") ) actionType = "rwak"; - const weapon = this.item.system; const ammo = this.actor?.items.get(ammunition)?.system; const { parts, data } = CONFIG.Dice.BasicRoll.constructParts({ @@ -278,7 +267,7 @@ export default class AttackActivityData extends BaseActivityData { bonus: this.attack.bonus, weaponMagic: weapon.magicAvailable ? weapon.magicalBonus : null, ammoMagic: ammo?.magicAvailable ? ammo.magicalBonus : null, - actorBonus: this.actor?.system.bonuses?.[actionType]?.attack, + actorBonus: this.actor?.system.bonuses?.[this.getActionType(attackMode)]?.attack, situational }, rollData); @@ -420,7 +409,7 @@ export default class AttackActivityData extends BaseActivityData { } const criticalBonusDice = this.actor?.getFlag("dnd5e", "meleeCriticalDamageDice") ?? 0; - if ( (this.actionType === "mwak") && (parseInt(criticalBonusDice) !== 0) ) { + if ( (this.getActionType(rollConfig.attackMode) === "mwak") && (parseInt(criticalBonusDice) !== 0) ) { foundry.utils.setProperty(roll, "options.critical.bonusDice", criticalBonusDice); } diff --git a/module/data/activity/base-activity.mjs b/module/data/activity/base-activity.mjs index 1e6675c6be..b122d55ac0 100644 --- a/module/data/activity/base-activity.mjs +++ b/module/data/activity/base-activity.mjs @@ -648,6 +648,19 @@ export default class BaseActivityData extends foundry.abstract.DataModel { /* Helpers */ /* -------------------------------------------- */ + /** + * Retrieve the action type reflecting changes based on the provided attack mode. + * @param {string} [attackMode=""] + * @returns {string} + */ + getActionType(attackMode="") { + let actionType = this.actionType; + if ( (actionType === "mwak") && (attackMode?.startsWith("thrown") || (attackMode === "ranged")) ) return "rwak"; + return actionType; + } + + /* -------------------------------------------- */ + /** * Get the roll parts used to create the damage rolls. * @param {Partial} [config={}] @@ -683,8 +696,7 @@ export default class BaseActivityData extends foundry.abstract.DataModel { const data = { ...rollData }; if ( index === 0 ) { - let actionType = this.actionType; - if ( (actionType === "mwak") && rollConfig.attackMode?.startsWith("thrown") ) actionType = "rwak"; + const actionType = this.getActionType(rollConfig.attackMode); const bonus = foundry.utils.getProperty(this.actor ?? {}, `system.bonuses.${actionType}.damage`); if ( bonus && (parseInt(bonus) !== 0) ) parts.push(bonus); } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 599241ec5f..b4a8f3b96a 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -175,7 +175,8 @@ export default class WeaponData extends ItemDataModel.mixin( * @param {object} source The candidate source data from which the model will be constructed. */ static #migrateReach(source) { - if ( !source.properties || !source.range?.value || !source.type?.value || source.range?.reach ) return; + if ( !source.properties || !source.range?.value || !source.type?.value + || (source.range?.reach !== undefined) ) return; if ( (CONFIG.DND5E.weaponTypeMap[source.type.value] !== "melee") || source.properties.includes("thr") ) return; // Range of `0` or greater than `10` is always included, and so is range longer than `5` without reach property if ( (source.range.value === 0) || (source.range.value > 10) @@ -351,6 +352,11 @@ export default class WeaponData extends ItemDataModel.mixin( }); } + else if ( !this.attackType && this.range.value ) { + if ( modes.length ) modes.push({ rule: true }); + modes.push({ value: "ranged", label: CONFIG.DND5E.attackModes.ranged.label }); + } + return modes; } @@ -510,6 +516,21 @@ export default class WeaponData extends ItemDataModel.mixin( return Number(isProficient); } + /* -------------------------------------------- */ + + /** + * Attack types that can be used with this item by default. + * @type {Set} + */ + get validAttackTypes() { + const types = new Set(); + const attackType = this.attackType; + if ( (attackType === "melee") || (attackType === null) ) types.add("melee"); + if ( (attackType === "ranged") || this.properties.has("thr") + || ((attackType === null) && this.range.value) ) types.add("ranged"); + return types; + } + /* -------------------------------------------- */ /* Socket Event Handlers */ /* -------------------------------------------- */ diff --git a/module/enrichers.mjs b/module/enrichers.mjs index 31e9832925..aee563d174 100644 --- a/module/enrichers.mjs +++ b/module/enrichers.mjs @@ -188,7 +188,7 @@ async function enrichAttack(config, label, options) { config.type = "attack"; if ( label ) return createRollLink(label, config); - let displayFormula = simplifyRollFormula(config.formula); + let displayFormula = simplifyRollFormula(config.formula) || "+0"; if ( !displayFormula.startsWith("+") && !displayFormula.startsWith("-") ) displayFormula = `+${displayFormula}`; const span = document.createElement("span");