Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow natural weapons to make ranged attacks without "thrown" #4901

Open
wants to merge 1 commit into
base: 4.2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@
"Label": "Attack Mode",
"Offhand": "Offhand",
"OneHanded": "One-Handed",
"Ranged": "Ranged",
"Thrown": "Thrown",
"ThrownOffhand": "Offhand Throw",
"TwoHanded": "Two-Handed"
Expand Down
6 changes: 4 additions & 2 deletions module/applications/activity/attack-sheet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
)
})
});

Expand Down
3 changes: 3 additions & 0 deletions module/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
19 changes: 4 additions & 15 deletions module/data/activity/attack-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/* -------------------------------------------- */
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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({
Expand All @@ -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);

Expand Down Expand Up @@ -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);
}

Expand Down
16 changes: 14 additions & 2 deletions module/data/activity/base-activity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DamageRollProcessConfiguration>} [config={}]
Expand Down Expand Up @@ -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);
}
Expand Down
23 changes: 22 additions & 1 deletion module/data/item/weapon.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<string>}
*/
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 */
/* -------------------------------------------- */
Expand Down
2 changes: 1 addition & 1 deletion module/enrichers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down