diff --git a/src/scripts/ui/inline-roll-links.ts b/src/scripts/ui/inline-roll-links.ts index 419068475f4..c24b09da98e 100644 --- a/src/scripts/ui/inline-roll-links.ts +++ b/src/scripts/ui/inline-roll-links.ts @@ -61,215 +61,243 @@ export const InlineRollLinks = { InlineRollLinks.flavorDamageRolls(html, foundryDoc instanceof ActorPF2e ? foundryDoc : null); for (const link of links.filter((l) => l.dataset.pf2Action)) { - const { pf2Action, pf2Glyph, pf2Variant, pf2Dc, pf2ShowDc, pf2Skill } = link.dataset; link.addEventListener("click", (event) => { - const slug = sluggify(pf2Action ?? ""); - const visibility = pf2ShowDc ?? "all"; - const difficultyClass = Number.isNumeric(pf2Dc) - ? { scope: "check", value: Number(pf2Dc) || 0, visibility } - : pf2Dc; - if (slug && game.pf2e.actions.has(slug)) { - game.pf2e.actions - .get(slug) - ?.use({ event, variant: pf2Variant, difficultyClass, statistic: pf2Skill }) - .catch((reason: string) => ui.notifications.warn(reason)); - } else { - const action = game.pf2e.actions[pf2Action ? sluggify(pf2Action, { camel: "dromedary" }) : ""]; - if (pf2Action && action) { - action({ - event, - glyph: pf2Glyph, - variant: pf2Variant, - difficultyClass, - skill: pf2Skill, - }); - } else { - console.warn(`PF2e System | Skip executing unknown action '${pf2Action}'`); - } - } + InlineRollLinks._onClickInlineAction(event, link); }); } for (const link of links.filter((l) => l.dataset.pf2Check && !l.dataset.invalid)) { - const { pf2Check, pf2Dc, pf2Traits, pf2Label, pf2Defense, pf2Adjustment, pf2Roller, pf2RollOptions } = - link.dataset; - const overrideTraits = "overrideTraits" in link.dataset; - const targetOwner = "targetOwner" in link.dataset; + link.addEventListener("click", async (event) => { + InlineRollLinks._onClickInlineCheck(event, link, foundryDoc); + }); + } - if (!pf2Check) return; + for (const link of links.filter((l) => l.hasAttribute("data-pf2-effect-area"))) { + link.addEventListener("click", (event) => { + InlineRollLinks._onClickInlineTemplate(event, html, link, foundryDoc); + }); + } + }, - link.addEventListener("click", async (event) => { - const parent = resolveActor(foundryDoc, link); - const actors = ((): ActorPF2e[] => { - switch (pf2Roller) { - case "self": - return parent?.canUserModify(game.user, "update") ? [parent] : []; - case "party": - if (parent?.isOfType("party")) return [parent]; - return R.compact([game.actors.party]); - } + makeRepostHtml: (target: HTMLElement, defaultVisibility: string): string => { + const flavor = game.i18n.localize(target.dataset.pf2RepostFlavor ?? ""); + const showDC = target.dataset.pf2ShowDc ?? defaultVisibility; + return `${flavor} ${target.outerHTML}`.trim(); + }, - // Use the DOM document as a fallback if it's an actor and the check isn't a saving throw - const sheetActor = ((): ActorPF2e | null => { - const maybeActor: ActorPF2e | null = - foundryDoc instanceof ActorPF2e - ? foundryDoc - : foundryDoc instanceof ItemPF2e && foundryDoc.actor - ? foundryDoc.actor - : null; - return maybeActor?.isOwner && !maybeActor.isOfType("loot", "party") ? maybeActor : null; + _onClickInlineAction: (event: MouseEvent, link: HTMLAnchorElement | HTMLSpanElement): void => { + const { pf2Action, pf2Glyph, pf2Variant, pf2Dc, pf2ShowDc, pf2Skill } = link.dataset; + + const slug = sluggify(pf2Action ?? ""); + const visibility = pf2ShowDc ?? "all"; + const difficultyClass = Number.isNumeric(pf2Dc) + ? { scope: "check", value: Number(pf2Dc) || 0, visibility } + : pf2Dc; + if (slug && game.pf2e.actions.has(slug)) { + game.pf2e.actions + .get(slug) + ?.use({ event, variant: pf2Variant, difficultyClass, statistic: pf2Skill }) + .catch((reason: string) => ui.notifications.warn(reason)); + } else { + const action = game.pf2e.actions[pf2Action ? sluggify(pf2Action, { camel: "dromedary" }) : ""]; + if (pf2Action && action) { + action({ + event, + glyph: pf2Glyph, + variant: pf2Variant, + difficultyClass, + skill: pf2Skill, + }); + } else { + console.warn(`PF2e System | Skip executing unknown action '${pf2Action}'`); + } + } + }, + + _onClickInlineCheck: async ( + event: MouseEvent, + link: HTMLAnchorElement | HTMLSpanElement, + foundryDoc: ClientDocument | null, + ): Promise => { + const { pf2Check, pf2Dc, pf2Traits, pf2Label, pf2Defense, pf2Adjustment, pf2Roller, pf2RollOptions } = + link.dataset; + const overrideTraits = "overrideTraits" in link.dataset; + const targetOwner = "targetOwner" in link.dataset; + + if (!pf2Check) return; + + const parent = resolveActor(foundryDoc, link); + const actors = ((): ActorPF2e[] => { + switch (pf2Roller) { + case "self": + return parent?.canUserModify(game.user, "update") ? [parent] : []; + case "party": + if (parent?.isOfType("party")) return [parent]; + return R.compact([game.actors.party]); + } + + // Use the DOM document as a fallback if it's an actor and the check isn't a saving throw + const sheetActor = ((): ActorPF2e | null => { + const maybeActor: ActorPF2e | null = + foundryDoc instanceof ActorPF2e + ? foundryDoc + : foundryDoc instanceof ItemPF2e && foundryDoc.actor + ? foundryDoc.actor + : null; + return maybeActor?.isOwner && !maybeActor.isOfType("loot", "party") ? maybeActor : null; + })(); + const rollingActors = [ + sheetActor ?? getSelectedActors({ exclude: ["loot"], assignedFallback: true }), + ].flat(); + + const isSave = tupleHasValue(SAVE_TYPES, pf2Check); + if (parent?.isOfType("party") || (rollingActors.length === 0 && parent && !isSave)) { + return [parent]; + } + + return rollingActors; + })(); + + if (actors.length === 0) { + ui.notifications.error("PF2E.ErrorMessage.NoTokenSelected", { localize: true }); + return; + } + + const extraRollOptions = [ + ...(pf2Traits?.split(",").map((o) => o.trim()) ?? []), + ...(pf2RollOptions?.split(",").map((o) => o.trim()) ?? []), + ]; + const eventRollParams = eventToRollParams(event, { type: "check" }); + const checkSlug = link.dataset.slug ? sluggify(link.dataset.slug) : null; + + switch (pf2Check) { + case "flat": { + for (const actor of actors) { + const flatCheck = new Statistic(actor, { + label: "", + slug: "flat", + modifiers: [], + check: { type: "flat-check" }, + }); + const dc = Number.isInteger(Number(pf2Dc)) ? { label: pf2Label, value: Number(pf2Dc) } : null; + flatCheck.roll({ ...eventRollParams, slug: checkSlug, extraRollOptions, dc }); + } + break; + } + default: { + const isSavingThrow = tupleHasValue(SAVE_TYPES, pf2Check); + + // Get actual traits for display in chat cards + const traits = isSavingThrow + ? [] + : extraRollOptions.filter((t): t is ActionTrait => t in CONFIG.PF2E.actionTraits) ?? []; + + for (const actor of actors) { + const statistic = ((): Statistic | null => { + if (pf2Check in CONFIG.PF2E.magicTraditions && actor.isOfType("creature")) { + const bestSpellcasting = + actor.spellcasting + .filter((c) => c.tradition === pf2Check) + .flatMap((s) => s.statistic ?? []) + .sort((a, b) => b.check.mod - a.check.mod) + .shift() ?? null; + if (bestSpellcasting) return bestSpellcasting; + } + return actor.getStatistic(pf2Check); })(); - const rollingActors = [ - sheetActor ?? getSelectedActors({ exclude: ["loot"], assignedFallback: true }), - ].flat(); - const isSave = tupleHasValue(SAVE_TYPES, pf2Check); - if (parent?.isOfType("party") || (rollingActors.length === 0 && parent && !isSave)) { - return [parent]; + if (!statistic) { + console.warn(ErrorPF2e(`Skip rolling unknown statistic ${pf2Check}`).message); + continue; } - return rollingActors; - })(); + const targetActor = pf2Defense ? (targetOwner ? parent : game.user.targets.first()?.actor) : null; - if (actors.length === 0) { - ui.notifications.error("PF2E.ErrorMessage.NoTokenSelected", { localize: true }); - return; - } + const dcValue = (() => { + const adjustment = Number(pf2Adjustment) || 0; + if (pf2Dc === "@self.level") { + return calculateDC(actor.level) + adjustment; + } + return Number(pf2Dc ?? "NaN") + adjustment; + })(); - const extraRollOptions = [ - ...(pf2Traits?.split(",").map((o) => o.trim()) ?? []), - ...(pf2RollOptions?.split(",").map((o) => o.trim()) ?? []), - ]; - const eventRollParams = eventToRollParams(event, { type: "check" }); - const checkSlug = link.dataset.slug ? sluggify(link.dataset.slug) : null; - - switch (pf2Check) { - case "flat": { - for (const actor of actors) { - const flatCheck = new Statistic(actor, { - label: "", - slug: "flat", - modifiers: [], - check: { type: "flat-check" }, - }); - const dc = Number.isInteger(Number(pf2Dc)) - ? { label: pf2Label, value: Number(pf2Dc) } + const dc = ((): CheckDC | null => { + if (Number.isInteger(dcValue)) { + return { label: pf2Label, value: dcValue }; + } else if (pf2Defense) { + const defenseStat = targetActor?.getStatistic(pf2Defense); + return defenseStat + ? { + statistic: defenseStat.dc, + scope: "check", + value: defenseStat.dc.value, + } : null; - flatCheck.roll({ ...eventRollParams, slug: checkSlug, extraRollOptions, dc }); } - break; - } - default: { - const isSavingThrow = tupleHasValue(SAVE_TYPES, pf2Check); - - // Get actual traits for display in chat cards - const traits = isSavingThrow - ? [] - : extraRollOptions.filter((t): t is ActionTrait => t in CONFIG.PF2E.actionTraits) ?? []; - - for (const actor of actors) { - const statistic = ((): Statistic | null => { - if (pf2Check in CONFIG.PF2E.magicTraditions && actor.isOfType("creature")) { - const bestSpellcasting = - actor.spellcasting - .filter((c) => c.tradition === pf2Check) - .flatMap((s) => s.statistic ?? []) - .sort((a, b) => b.check.mod - a.check.mod) - .shift() ?? null; - if (bestSpellcasting) return bestSpellcasting; - } - return actor.getStatistic(pf2Check); - })(); - - if (!statistic) { - console.warn(ErrorPF2e(`Skip rolling unknown statistic ${pf2Check}`).message); - continue; - } - - const targetActor = pf2Defense - ? targetOwner - ? parent - : game.user.targets.first()?.actor - : null; + return null; + })(); - const dcValue = (() => { - const adjustment = Number(pf2Adjustment) || 0; - if (pf2Dc === "@self.level") { - return calculateDC(actor.level) + adjustment; - } - return Number(pf2Dc ?? "NaN") + adjustment; - })(); - - const dc = ((): CheckDC | null => { - if (Number.isInteger(dcValue)) { - return { label: pf2Label, value: dcValue }; - } else if (pf2Defense) { - const defenseStat = targetActor?.getStatistic(pf2Defense); - return defenseStat - ? { - statistic: defenseStat.dc, - scope: "check", - value: defenseStat.dc.value, - } - : null; - } - return null; - })(); - - // Retrieve the item if: - // (2) The item is an action or, - // (1) The check is a saving throw and the item is not a weapon. - // Exclude weapons so that roll notes on strikes from incapacitation abilities continue to work. - const item = (() => { - const itemFromDoc = - foundryDoc instanceof ItemPF2e - ? foundryDoc - : foundryDoc instanceof ChatMessagePF2e - ? foundryDoc.item - : null; - - return itemFromDoc?.isOfType("action", "feat", "campaignFeature") || - (isSavingThrow && !itemFromDoc?.isOfType("weapon")) - ? itemFromDoc - : null; - })(); - - const args: StatisticRollParameters = { - ...eventRollParams, - extraRollOptions, - origin: isSavingThrow && parent instanceof ActorPF2e ? parent : null, - dc, - target: !isSavingThrow && dc?.statistic ? targetActor : null, - item, - traits, - }; - - // Use a special header for checks against defenses - const itemIsEncounterAction = - !overrideTraits && - !!(item?.isOfType("action", "feat") && item.actionCost) && - !["flat-check", "saving-throw"].includes(statistic.check.type); - if (itemIsEncounterAction) { - const subtitleLocKey = - pf2Check in CONFIG.PF2E.magicTraditions - ? "PF2E.ActionsCheck.spell" - : statistic.check.type === "attack-roll" - ? "PF2E.ActionsCheck.x-attack-roll" - : "PF2E.ActionsCheck.x"; - args.label = await renderTemplate("systems/pf2e/templates/chat/action/header.hbs", { - glyph: getActionGlyph(item.actionCost), - subtitle: game.i18n.format(subtitleLocKey, { type: statistic.label }), - title: item.name, - }); - extraRollOptions.push(...TextEditorPF2e.createActionOptions(item)); - } - - statistic.roll(args); - } + // Retrieve the item if: + // (2) The item is an action or, + // (1) The check is a saving throw and the item is not a weapon. + // Exclude weapons so that roll notes on strikes from incapacitation abilities continue to work. + const item = (() => { + const itemFromDoc = + foundryDoc instanceof ItemPF2e + ? foundryDoc + : foundryDoc instanceof ChatMessagePF2e + ? foundryDoc.item + : null; + + return itemFromDoc?.isOfType("action", "feat", "campaignFeature") || + (isSavingThrow && !itemFromDoc?.isOfType("weapon")) + ? itemFromDoc + : null; + })(); + + const args: StatisticRollParameters = { + ...eventRollParams, + extraRollOptions, + origin: isSavingThrow && parent instanceof ActorPF2e ? parent : null, + dc, + target: !isSavingThrow && dc?.statistic ? targetActor : null, + item, + traits, + }; + + // Use a special header for checks against defenses + const itemIsEncounterAction = + !overrideTraits && + !!(item?.isOfType("action", "feat") && item.actionCost) && + !["flat-check", "saving-throw"].includes(statistic.check.type); + if (itemIsEncounterAction) { + const subtitleLocKey = + pf2Check in CONFIG.PF2E.magicTraditions + ? "PF2E.ActionsCheck.spell" + : statistic.check.type === "attack-roll" + ? "PF2E.ActionsCheck.x-attack-roll" + : "PF2E.ActionsCheck.x"; + args.label = await renderTemplate("systems/pf2e/templates/chat/action/header.hbs", { + glyph: getActionGlyph(item.actionCost), + subtitle: game.i18n.format(subtitleLocKey, { type: statistic.label }), + title: item.name, + }); + extraRollOptions.push(...TextEditorPF2e.createActionOptions(item)); } + + statistic.roll(args); } - }); + } } + }, + + _onClickInlineTemplate: ( + _event: MouseEvent, + html: HTMLElement, + link: HTMLAnchorElement | HTMLSpanElement, + foundryDoc: ClientDocument | null, + ): void => { + if (!canvas.ready) return; const templateConversion: Record = { burst: "circle", @@ -281,80 +309,68 @@ export const InlineRollLinks = { square: "rect", } as const; - for (const link of links.filter((l) => l.hasAttribute("data-pf2-effect-area"))) { - const { pf2EffectArea, pf2Distance, pf2TemplateData, pf2Traits, pf2Width } = link.dataset; - link.addEventListener("click", () => { - if (!canvas.ready) return; - - if (typeof pf2EffectArea !== "string") { - console.warn(`PF2e System | Could not create template'`); - return; - } + const { pf2EffectArea, pf2Distance, pf2TemplateData, pf2Traits, pf2Width } = link.dataset; - const data: DeepPartial = JSON.parse(pf2TemplateData ?? "{}"); - data.distance ||= Number(pf2Distance); - data.fillColor ||= game.user.color; - data.t = templateConversion[pf2EffectArea]; - - switch (data.t) { - case "ray": - data.width = - Number(pf2Width) || CONFIG.MeasuredTemplate.defaults.width * canvas.dimensions.distance; - break; - case "cone": - data.angle ||= CONFIG.MeasuredTemplate.defaults.angle; - break; - case "rect": { - const distance = data.distance ?? 0; - data.distance = Math.hypot(distance, distance); - data.width = distance; - data.direction = 45; - break; - } - } + if (typeof pf2EffectArea !== "string") { + console.warn(`PF2e System | Could not create template'`); + return; + } - const flags: { pf2e: Record } = { - pf2e: {}, - }; + const data: DeepPartial = JSON.parse(pf2TemplateData ?? "{}"); + data.distance ||= Number(pf2Distance); + data.fillColor ||= game.user.color; + data.t = templateConversion[pf2EffectArea]; + + switch (data.t) { + case "ray": + data.width = Number(pf2Width) || CONFIG.MeasuredTemplate.defaults.width * canvas.dimensions.distance; + break; + case "cone": + data.angle ||= CONFIG.MeasuredTemplate.defaults.angle; + break; + case "rect": { + const distance = data.distance ?? 0; + data.distance = Math.hypot(distance, distance); + data.width = distance; + data.direction = 45; + break; + } + } - const normalSize = (Math.ceil(data.distance) / 5) * 5 || 5; - if (tupleHasValue(EFFECT_AREA_SHAPES, pf2EffectArea) && data.distance === normalSize) { - flags.pf2e.areaShape = pf2EffectArea; - } + const flags: { pf2e: Record } = { + pf2e: {}, + }; - const messageId = - foundryDoc instanceof ChatMessagePF2e - ? foundryDoc.id - : htmlClosest(html, "[data-message-id]")?.dataset.messageId ?? null; - if (messageId) { - flags.pf2e.messageId = messageId; - } + const normalSize = (Math.ceil(data.distance) / 5) * 5 || 5; + if (tupleHasValue(EFFECT_AREA_SHAPES, pf2EffectArea) && data.distance === normalSize) { + flags.pf2e.areaShape = pf2EffectArea; + } - const actor = resolveActor(foundryDoc, link); - if (actor || pf2Traits) { - const origin: Record = {}; - if (actor) { - origin.actor = actor.uuid; - } - if (pf2Traits) { - origin.traits = pf2Traits.split(","); - } - flags.pf2e.origin = origin; - } + const messageId = + foundryDoc instanceof ChatMessagePF2e + ? foundryDoc.id + : htmlClosest(html, "[data-message-id]")?.dataset.messageId ?? null; + if (messageId) { + flags.pf2e.messageId = messageId; + } - if (!R.isEmpty(flags.pf2e)) { - data.flags = flags; - } + const actor = resolveActor(foundryDoc, link); + if (actor || pf2Traits) { + const origin: Record = {}; + if (actor) { + origin.actor = actor.uuid; + } + if (pf2Traits) { + origin.traits = pf2Traits.split(","); + } + flags.pf2e.origin = origin; + } - canvas.templates.createPreview(data); - }); + if (!R.isEmpty(flags.pf2e)) { + data.flags = flags; } - }, - makeRepostHtml: (target: HTMLElement, defaultVisibility: string): string => { - const flavor = game.i18n.localize(target.dataset.pf2RepostFlavor ?? ""); - const showDC = target.dataset.pf2ShowDc ?? defaultVisibility; - return `${flavor} ${target.outerHTML}`.trim(); + canvas.templates.createPreview(data); }, repostAction: async ( diff --git a/static/system.json b/static/system.json index 5a306bb4d6f..628b69a6c8d 100644 --- a/static/system.json +++ b/static/system.json @@ -5,8 +5,8 @@ "version": "5.15.2", "license": "./LICENSE", "compatibility": { - "minimum": "11.315", - "verified": "11.315", + "minimum": "12.320", + "verified": "12.321", "maximum": "12" }, "authors": [