diff --git a/README.md b/README.md index 3cf19641..611c80d0 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,13 @@ Automatically add +1 to attack rolls to weapons with `Weapon Focus`. Includes `G - The dropdown will be added automatically if you add a dictionary flag `greater-weapon-focus` to the feat (or any other Item) - The choices will be based off of any other `Weapon Focus` feats you already have configured. + ### Mythic Weapon Focus + Doubles the bonus from `Weapon Focus` and `Greater Weapon Focus` + - Will automatically include the select input in the feat advanced tab if the feat name includes both `Weapon Focus` and `Mythic` + - This is configurable in the settings to account for different translations + - The dropdown will be added automatically if you add a dictionary flag `mythic-weapon-focus` to the feat (or any other Item) + - The choices will be based off of any other `Weapon Focus` feats you already have configured. + ### Racial Weapon Focus Adds +1 to hit to racial weapons - those weapons must have appropriate racial tags. - Will Automatically include the select input in the feat advanced tab if the feat is named `Gnome Weapon Focus` (only official racial weapon feat) diff --git a/lang/en.json b/lang/en.json index dfd04e16..0dcb5224 100644 --- a/lang/en.json +++ b/lang/en.json @@ -50,6 +50,7 @@ "misfortune": "Misfortune", "mythicElementalFocus": "Mythic Elemental Focus", "mythicSpellFocus": "Mythic Spell Focus", + "mythic-weapon-focus": "Mythic Weapon Focus", "ok": "OK", "roll-bonuses": "Roll Bonuses", "school-dc": "Spell School DC", @@ -130,6 +131,11 @@ "hint": "The 'Mythic' adjective, feat names are checked for this plus 'Spell Focus'. This is to support translations without having to translate the mod.", "default": "Mythic" }, + "mythic-weapon-focus": { + "name": "Mythic Weapon Focus", + "hint": "The 'Mythic' adjective, feat names are checked for this plus 'Weapon Focus'. This is to support translations without having to translate the mod.", + "default": "Mythic" + }, "racial-weapon-focus": { "name": "Racial Weapon Focus", "hint": "The name of your 'Racial Spell Focus' feat. Only 'Gnome Weapon Focus' officially exists, but this allows for other racial variations. This is to support translations without having to translate the mod.", @@ -172,4 +178,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/bonuses/weapon-focus/ids.mjs b/src/bonuses/weapon-focus/ids.mjs index a6fa8429..31fa4187 100644 --- a/src/bonuses/weapon-focus/ids.mjs +++ b/src/bonuses/weapon-focus/ids.mjs @@ -1,7 +1,9 @@ export const weaponFocusKey = 'weapon-focus'; export const greaterWeaponFocusKey = 'greater-weapon-focus'; +export const mythicWeaponFocusKey = 'mythic-weapon-focus'; export const racialWeaponFocusKey = 'racial-weapon-focus'; export const weaponFocusId = 'n250dFlbykAIAg5Z'; export const greaterWeaponFocusId = 'IER2MzJrjSvxMlNS'; +export const mythicWeaponFocusId = 'stJ6Jp1ALN6qgGBr'; export const gnomeWeaponFocusId = '8RzIeYtbx0UtXUge'; diff --git a/src/bonuses/weapon-focus/weapon-focus.mjs b/src/bonuses/weapon-focus/weapon-focus.mjs index 67274c0d..9a580ae8 100644 --- a/src/bonuses/weapon-focus/weapon-focus.mjs +++ b/src/bonuses/weapon-focus/weapon-focus.mjs @@ -6,31 +6,49 @@ import { localHooks } from "../../util/hooks.mjs"; import { registerItemHint } from "../../util/item-hints.mjs"; import { localize } from "../../util/localize.mjs"; import { registerSetting } from "../../util/settings.mjs"; +import { signed } from '../../util/to-signed-string.mjs'; import { uniqueArray } from "../../util/unique-array.mjs"; -import { gnomeWeaponFocusId, greaterWeaponFocusId, greaterWeaponFocusKey, racialWeaponFocusKey, weaponFocusId, weaponFocusKey } from "./ids.mjs"; - -const allKeys = [weaponFocusKey, greaterWeaponFocusKey]; +import { + gnomeWeaponFocusId, + greaterWeaponFocusId, + greaterWeaponFocusKey, + mythicWeaponFocusKey, + mythicWeaponFocusId, + racialWeaponFocusKey, + weaponFocusId, + weaponFocusKey, +} from "./ids.mjs"; + +const allKeys = [weaponFocusKey, greaterWeaponFocusKey, mythicWeaponFocusKey]; registerSetting({ key: weaponFocusKey }); registerSetting({ key: greaterWeaponFocusKey }); +registerSetting({ key: mythicWeaponFocusKey }); class Settings { static get weaponFocus() { return Settings.#getSetting(weaponFocusKey); } static get greater() { return Settings.#getSetting(greaterWeaponFocusKey); } + static get mythic() { return Settings.#getSetting(mythicWeaponFocusKey); } // @ts-ignore static #getSetting(/** @type {string} */key) { return game.settings.get(MODULE_NAME, key).toLowerCase(); } } // register hint on item with focus registerItemHint((hintcls, _actor, item, _data) => { - const /** @type {Hint[]} */ hints = []; - allKeys.forEach((key) => { - const current = item.getItemDictionaryFlag(key); - if (current) { - hints.push(hintcls.create(`${current}`, [], {})); - } - }); - return hints; + const key = allKeys.find((k) => item.system.flags.dictionary[k] !== undefined); + if (!key) { + return; + } + + const currentTarget = getDocDFlags(item, key)[0]; + if (!currentTarget) { + return; + } + + const label = `${currentTarget}`; + + const hint = hintcls.create(label, [], {}); + return hint; }); // register hint on focused weapon/attack @@ -41,18 +59,29 @@ registerItemHint((hintcls, actor, item, _data) => { const baseTypes = item.system.baseTypes; - const helper = new KeyedDFlagHelper(actor, weaponFocusKey, greaterWeaponFocusKey); + const helper = new KeyedDFlagHelper(actor, weaponFocusKey, greaterWeaponFocusKey, mythicWeaponFocusKey); - let label; - if (intersects(baseTypes, helper.valuesForFlag(greaterWeaponFocusKey))) { - label = localize(greaterWeaponFocusKey); - } - else if (intersects(baseTypes, helper.valuesForFlag(weaponFocusKey))) { - label = localize(weaponFocusKey); - } + const isFocused = intersects(baseTypes, helper.valuesForFlag(weaponFocusKey)); + const isGreater = intersects(baseTypes, helper.valuesForFlag(greaterWeaponFocusKey)); + const isMythic = intersects(baseTypes, helper.valuesForFlag(mythicWeaponFocusKey)); - if (label) { - return hintcls.create(label, [], {}); + if (isFocused || isGreater || isMythic) { + const tips = [] + let bonus = 0; + if (isFocused) { + tips.push(localize(weaponFocusKey)); + bonus += 1; + } + if (isGreater) { + tips.push(localize(greaterWeaponFocusKey)); + bonus += 1; + } + if (isMythic) { + tips.push(localize(mythicWeaponFocusKey)); + bonus *= 2; + } + tips.push(localize('dc-mod', { mod: signed(bonus) })); + return hintcls.create('', [], { icon: 'fas fa-sword', hint: tips.join('\n') }); } }); @@ -74,16 +103,19 @@ function getAttackSources(item, sources) { let value = 0; let name = localize(weaponFocusKey); - const weaponFocuses = getDocDFlags(actor, weaponFocusKey); - const greaterWeaponFocuses = getDocDFlags(actor, greaterWeaponFocusKey); + const helper = new KeyedDFlagHelper(actor, weaponFocusKey, greaterWeaponFocusKey, mythicWeaponFocusKey); - if (baseTypes.find(bt => weaponFocuses.includes(bt))) { + if (baseTypes.find(bt => helper.valuesForFlag(weaponFocusKey).includes(bt))) { value += 1; } - if (baseTypes.find(bt => greaterWeaponFocuses.includes(bt))) { + if (baseTypes.find(bt => helper.valuesForFlag(greaterWeaponFocusKey).includes(bt))) { value += 1; name = localize(greaterWeaponFocusKey); } + if (baseTypes.find(bt => helper.valuesForFlag(mythicWeaponFocusKey).includes(bt))) { + value *= 2; + name = localize(mythicWeaponFocusKey); + } if (value) { sources.push({ value, name, modifier: 'untyped', sort: -100, }); @@ -106,17 +138,24 @@ function addWeaponFocusBonus({ actor, item, shared }) { const baseTypes = item.system.baseTypes; let value = 0; - const helper = new KeyedDFlagHelper(actor, weaponFocusKey, greaterWeaponFocusKey); + const helper = new KeyedDFlagHelper(actor, weaponFocusKey, greaterWeaponFocusKey, mythicWeaponFocusKey); + let key = ''; - if (baseTypes.find(value => helper.valuesForFlag(weaponFocusKey).includes(value))) { + if (intersects(baseTypes, helper.valuesForFlag(weaponFocusKey))) { value += 1; + key = weaponFocusKey; } - if (baseTypes.find(value => helper.valuesForFlag(greaterWeaponFocusKey).includes(value))) { + if (intersects(baseTypes, helper.valuesForFlag(greaterWeaponFocusKey))) { value += 1; + key = greaterWeaponFocusKey; + } + if (intersects(baseTypes, helper.valuesForFlag(mythicWeaponFocusKey))) { + value *= 2; + key = mythicWeaponFocusKey; } if (value) { - shared.attackBonus.push(`${value}[${localize(weaponFocusKey)}]`); + shared.attackBonus.push(`${value}[${localize(key)}]`); } } Hooks.on(localHooks.actionUseAlterRollData, addWeaponFocusBonus); @@ -141,11 +180,15 @@ Hooks.on('renderItemSheet', ( const isGreater = (name.includes(Settings.weaponFocus) && name.includes(Settings.greater)) || sourceId.includes(greaterWeaponFocusId) || item.system.flags.dictionary[greaterWeaponFocusKey] !== undefined; + const isMythic = (name.includes(Settings.weaponFocus) && name.includes(Settings.mythic)) + || sourceId.includes(mythicWeaponFocusId) + || item.system.flags.dictionary[mythicWeaponFocusKey] !== undefined; const isRacial = sourceId.includes(gnomeWeaponFocusId) || item.system.flags.dictionary[racialWeaponFocusKey] !== undefined; - if (isGreater) { + if (isGreater || isMythic) { key = greaterWeaponFocusKey; + key = isGreater ? greaterWeaponFocusKey : mythicWeaponFocusKey; if (actor) { choices = getDocDFlags(actor, weaponFocusKey).map((x) => `${x}`); diff --git a/src/targeted/bonuses/damage-bonus.mjs b/src/targeted/bonuses/damage-bonus.mjs index 4e083ace..e12562dc 100644 --- a/src/targeted/bonuses/damage-bonus.mjs +++ b/src/targeted/bonuses/damage-bonus.mjs @@ -111,7 +111,7 @@ export class DamageBonus extends BaseBonus { sources = (conditional.modifiers ?? []) .filter((mod) => mod.target === 'damage') - .map((mod) => conditionalModToItemChange(conditional, mod, { isDamage: true })) + .map((mod) => conditionalModToItemChange(conditional, mod, { isDamage: true, rollData: target.getRollData() })) .filter(truthiness); return sources; diff --git a/src/util/conditional-helpers.mjs b/src/util/conditional-helpers.mjs index 16163926..46d1fdbe 100644 --- a/src/util/conditional-helpers.mjs +++ b/src/util/conditional-helpers.mjs @@ -67,9 +67,10 @@ export function conditionalCalculator(shared, conditional) { * @param {ItemConditionalModifier} modifier * @param {object} [options] * @param {boolean} [options.isDamage] + * @param {RollData?} [options.rollData] * @returns {Nullable} */ -export function conditionalModToItemChange(conditional, modifier, { isDamage = false } = {}) { +export function conditionalModToItemChange(conditional, modifier, { isDamage = false, rollData = null } = {}) { if (!modifier) return; const subTarget = modifier.target; @@ -92,7 +93,7 @@ export function conditionalModToItemChange(conditional, modifier, { isDamage = f operator: 'add', priority: 0, subTarget, - value: modifier.formula, + value: rollData ? RollPF.safeTotal(modifier.formula, rollData) : modifier.formula, }); if (isDamage) { change.type = modifier.type; diff --git a/types/pf1/pf1.d.ts b/types/pf1/pf1.d.ts index d6eb4335..98196ce4 100644 --- a/types/pf1/pf1.d.ts +++ b/types/pf1/pf1.d.ts @@ -1,16 +1,17 @@ import Document from '../foundry/common/abstract/document.mjs'; -export { }; +export {}; declare global { abstract class BaseDocument extends Document { + getRollData(): Nullable; getFlag(moduleName: string, key: string): any; async setFlag(moduleName: string, key: string, value: T); updateSource(changes: Partial, options?: object); uuid: string; } - abstract class ItemDocument extends BaseDocument { } + abstract class ItemDocument extends BaseDocument {} interface Abilities { str: 'Strength'; @@ -30,7 +31,7 @@ declare global { * Gets the actor's roll data. * @param refresh - pass true to force the roll data to recalculate * @returns The actor's roll data - */ + */ getRollData(args?: { refresh?: boolean }): RollData; id: string; @@ -61,7 +62,6 @@ declare global { }; } - type ConditionalPart = [number | string, TraitSelectorValuePlural, false]; class ConditionalPartsResults { 'attack.crit': string[]; @@ -159,7 +159,7 @@ declare global { /** @deprecated Spells don't have tags */ tag: string; } - interface ItemFeatPF extends ItemPF { } + interface ItemFeatPF extends ItemPF {} interface ItemLootPF extends ItemPF { subType: 'gear' | 'ammo' | 'tradeGoods' | 'misc'; } @@ -175,7 +175,7 @@ declare global { } interface SystemItem { - links: { children: { name: string, id: string }[], charges: unknown[] }; + links: { children: { name: string; id: string }[]; charges: unknown[] }; broken: boolean; flags: { boolean: {}; @@ -186,7 +186,7 @@ declare global { } interface SystemItemAttackPF extends SystemItem { baseTypes: string[]; - links: { children: { name: string, id: string }[] }; + links: { children: { name: string; id: string }[] }; weaponGroups: TraitSelector?; } interface SystemItemEquipmentPF extends SystemItem { @@ -197,7 +197,7 @@ declare global { value: number; }; baseTypes: string[]; - links: { children: { name: string, id: string }[] }; + links: { children: { name: string; id: string }[] }; proficient: boolean; slot: 'armor' | 'shield'; } @@ -207,7 +207,7 @@ declare global { } interface SystemWeaponPF extends SystemItem { baseTypes: string[]; - links: { children: { name: string, id: string }[] }; + links: { children: { name: string; id: string }[] }; proficient: boolean; weaponGroups: TraitSelector; } @@ -248,9 +248,8 @@ declare global { disposition: DispositionLevel; name: string; permission: PermissionLevel; - texture: { src: string; }; + texture: { src: string }; visible: boolean; - } interface ItemPF extends ItemDocument { @@ -521,14 +520,17 @@ declare global { static get defaultData(): any; constructor(obj: { [modifiers]: object[] }): ItemConditional; - static create(modifiers: object[], options: { - parent: { - data: { - conditionals: any[], - }, - update: any + static create( + modifiers: object[], + options: { + parent: { + data: { + conditionals: any[]; + }; + update: any; + }; } - }): ItemConditional; + ): ItemConditional; } class ItemConditionalModifier { @@ -539,23 +541,33 @@ declare global { formula: string; id?: string; subTarget: - | 'hasteAttack' | 'rapidShotAttack' | 'attack_0' | 'allAttack' // when target is 'attack' - | 'hasteDamage' | 'rapidShotDamage' | 'attack_0' | 'allDamage' // when target is 'damage' + | 'hasteAttack' + | 'rapidShotAttack' + | 'attack_0' + | 'allAttack' // when target is 'attack' + | 'hasteDamage' + | 'rapidShotDamage' + | 'attack_0' + | 'allDamage' // when target is 'damage' | 'dc' // when target is 'effect' | 'charges' // when target is 'misc' - | undefined // no subtarget for 'size' - ; + | undefined; // no subtarget for 'size' target: 'attack' | 'damage' | 'effect' | 'misc' | 'size'; type: Nullable; - - targets?: { attack: string; damage: string; size: string; effect: string; misc?: string; }; - subTargets?: { [x: string]: string; }; - conditionalModifierTypes?: { [x: string]: string; }; + targets?: { + attack: string; + damage: string; + size: string; + effect: string; + misc?: string; + }; + subTargets?: { [x: string]: string }; + conditionalModifierTypes?: { [x: string]: string }; conditionalCritical?: { - normal?: "PF1.Normal", - crit?: "PF1.CritDamageBonusFormula", - nonCrit?: "PF1.NonCritDamageBonusFormula", + normal?: 'PF1.Normal'; + crit?: 'PF1.CritDamageBonusFormula'; + nonCrit?: 'PF1.NonCritDamageBonusFormula'; }; constructor(any); @@ -596,15 +608,17 @@ declare global { } interface pf1 { applications: { - ActorTraitSelector: { new(doc: Document, options: object): ActorTraitSelector }; + ActorTraitSelector: { + new (doc: Document, options: object): ActorTraitSelector; + }; DamageTypeSelector: { - new( - object: { id: string, async update({ [dataPath]: object }) }, + new ( + object: { id: string; update({ [dataPath]: object }) }, dataPath: string, data: {}, - options = {}, - ): DamageTypeSelector - } + options = {} + ): DamageTypeSelector; + }; }; components: { ItemConditional: typeof ItemConditional; @@ -612,7 +626,7 @@ declare global { ItemAction: typeof ItemAction; // ItemAction: ItemAction ; ItemChange: { - new( + new ( args: { flavor: string; formula: string | number; @@ -654,21 +668,21 @@ declare global { abilities; bonusModifiers: BonusModifers; damageTypes: { - "untyped": "Untyped", - "slashing": "Slashing", - "piercing": "Piercing", - "bludgeoning": "Bludgeoning", - "fire": "Fire", - "cold": "Cold", - "electric": "Electricity", - "acid": "Acid", - "sonic": "Sonic", - "force": "Force", - "negative": "Negative", - "positive": "Positive", - "precision": "Precision", - "nonlethal": "Nonlethal" - }, + untyped: 'Untyped'; + slashing: 'Slashing'; + piercing: 'Piercing'; + bludgeoning: 'Bludgeoning'; + fire: 'Fire'; + cold: 'Cold'; + electric: 'Electricity'; + acid: 'Acid'; + sonic: 'Sonic'; + force: 'Force'; + negative: 'Negative'; + positive: 'Positive'; + precision: 'Precision'; + nonlethal: 'Nonlethal'; + }; savingThrows: SavingThrows; skillCompendiumEntries: { [key: string]: string }; skills; @@ -677,16 +691,16 @@ declare global { }; documents: { actor: { - ActorPF: { new(): ActorPF }; + ActorPF: { new (): ActorPF }; }; item: { - ItemAttackPF: { new(): ItemAttackPF }; - ItemEquipmentPF: { new(): ItemEquipmentPF }; - ItemFeatPF: { new(): ItemFeatPF }; - ItemLootPF: { new(): ItemLootPF }; - ItemPF: { new(): ItemPF }; - ItemSpellPF: { new(): ItemSpellPF }; - ItemWeaponPF: { new(): ItemWeaponPF }; + ItemAttackPF: { new (): ItemAttackPF }; + ItemEquipmentPF: { new (): ItemEquipmentPF }; + ItemFeatPF: { new (): ItemFeatPF }; + ItemLootPF: { new (): ItemLootPF }; + ItemPF: { new (): ItemPF }; + ItemSpellPF: { new (): ItemSpellPF }; + ItemWeaponPF: { new (): ItemWeaponPF }; }; }; registry: {