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

Add support for registering spells to staves #17994

Open
wants to merge 1 commit into
base: master
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
14 changes: 14 additions & 0 deletions build/lib/compendium-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ class CompendiumPack {

if (itemIsOfType(docSource, "physical")) {
docSource.system.equipped = { carryType: "worn" };

// Staff spell and name is only used to correct from broken links, but our uuid system handles that
if (itemIsOfType(docSource, "weapon") && docSource.system.staff?.spells) {
for (const spell of docSource.system.staff.spells) {
delete spell.name;
delete spell.img;
}
}
} else if (docSource.type === "feat") {
const featCategory = docSource.system.category;
if (!setHasElement(FEAT_OR_FEATURE_CATEGORIES, featCategory)) {
Expand Down Expand Up @@ -337,6 +345,12 @@ class CompendiumPack {
);
}

if (itemIsOfType(source, "weapon") && source.system.staff?.spells) {
for (const spell of source.system.staff.spells) {
spell.uuid = CompendiumPack.convertUUID(spell.uuid, convertOptions);
}
}

if (itemIsOfType(source, "feat", "action") && source.system.selfEffect) {
source.system.selfEffect.uuid = CompendiumPack.convertUUID(source.system.selfEffect.uuid, convertOptions);
} else if (itemIsOfType(source, "ancestry", "background", "class", "kit")) {
Expand Down
13 changes: 12 additions & 1 deletion src/module/item/base/sheet/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,10 @@ class ItemSheetPF2e<TItem extends ItemPF2e> extends ItemSheet<TItem, ItemSheetOp
tagify(htmlQuery<HTMLTagifyTagsElement>(html, 'tagify-tags[name="system.traits.otherTags"]'), { maxTags: 6 });

// Handle select and input elements that show modified prepared values until focused
const modifiedPropertyFields = htmlQueryAll<HTMLSelectElement | HTMLInputElement>(html, "[data-property]");
const modifiedPropertyFields = htmlQueryAll<HTMLSelectElement | HTMLInputElement>(
html,
"input[data-property], select[data-property]",
);
for (const input of modifiedPropertyFields) {
const propertyPath = input.dataset.property ?? "";
const baseValue =
Expand All @@ -503,6 +506,14 @@ class ItemSheetPF2e<TItem extends ItemPF2e> extends ItemSheet<TItem, ItemSheetOp
});
}

// Handle contenteditable fields
for (const input of htmlQueryAll<HTMLSpanElement>(html, "span[contenteditable][data-property]")) {
const propertyPath = input.dataset.property ?? "";
input.addEventListener("blur", () => {
this.item.update({ [propertyPath]: input.textContent });
});
}

// Add a link to add GM notes
if (
this.isEditable &&
Expand Down
4 changes: 2 additions & 2 deletions src/module/item/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class ItemChatData {
}

async #prepareDescription(): Promise<Pick<ItemDescriptionData, "value" | "gm">> {
const { data, item } = this;
const item = this.item;
const actor = item.actor;
const rollOptions = new Set([actor?.getRollOptions(), item.getRollOptions("item")].flat().filter(R.isTruthy));
const description = await this.item.getDescription();
Expand Down Expand Up @@ -129,7 +129,7 @@ class ItemChatData {

const templatePath = "systems/pf2e/templates/items/partials/addendum.hbs";
return Promise.all(
data.description.addenda.flatMap((unfiltered) => {
description.addenda.flatMap((unfiltered) => {
const addendum = {
label: game.i18n.localize(unfiltered.label),
contents: unfiltered.contents
Expand Down
6 changes: 3 additions & 3 deletions src/module/item/spellcasting-entry/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ItemPF2e, SpellPF2e, SpellcastingEntryPF2e } from "@item";
import { OneToTen, ValueAndMax, ZeroToTen } from "@module/data.ts";
import { ErrorPF2e, groupBy, localizer, ordinalString } from "@util";
import * as R from "remeda";
import { spellSlotGroupIdToNumber } from "./helpers.ts";
import { getSpellRankLabel, spellSlotGroupIdToNumber } from "./helpers.ts";
import { BaseSpellcastingEntry, SpellPrepEntry, SpellcastingSlotGroup } from "./types.ts";

class SpellCollection<TActor extends ActorPF2e> extends Collection<SpellPF2e<TActor>> {
Expand Down Expand Up @@ -367,8 +367,8 @@ class SpellCollection<TActor extends ActorPF2e> extends Collection<SpellPF2e<TAc
#warnInvalidDrop(warning: DropWarningType, { spell, groupId }: WarnInvalidDropParams): void {
const localize = localizer("PF2E.Item.Spell.Warning");
if (warning === "invalid-rank" && typeof groupId === "number") {
const spellRank = game.i18n.format("PF2E.Item.Spell.Rank.Ordinal", { rank: ordinalString(spell.baseRank) });
const targetRank = game.i18n.format("PF2E.Item.Spell.Rank.Ordinal", { rank: ordinalString(groupId) });
const spellRank = getSpellRankLabel(spell.baseRank);
const targetRank = getSpellRankLabel(groupId);
ui.notifications.warn(localize("InvalidRank", { spell: spell.name, spellRank, targetRank }));
} else if (warning === "cantrip-mismatch") {
const locKey = spell.isCantrip ? "CantripToRankedSlots" : "NonCantripToCantrips";
Expand Down
10 changes: 9 additions & 1 deletion src/module/item/spellcasting-entry/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ActorPF2e } from "@actor";
import type { OneToTen, ZeroToTen } from "@module/data.ts";
import { Statistic } from "@system/statistic/statistic.ts";
import { ordinalString } from "@util/misc.ts";
import * as R from "remeda";
import type { SpellSlotGroupId } from "./collection.ts";
import type { SpellcastingEntry } from "./types.ts";
Expand Down Expand Up @@ -38,4 +39,11 @@ function coerceToSpellGroupId(value: unknown): SpellSlotGroupId | null {
return numericValue.between(1, 10) ? (numericValue as OneToTen) : null;
}

export { coerceToSpellGroupId, createCounteractStatistic, spellSlotGroupIdToNumber };
/** Returns the label for a rank header, such as "1st Rank" */
function getSpellRankLabel(group: "cantrips" | number): string {
return group === 0 || group === "cantrips"
? game.i18n.localize("PF2E.Actor.Creature.Spellcasting.Cantrips")
: game.i18n.format("PF2E.Item.Spell.Rank.Ordinal", { rank: ordinalString(group) });
}

export { coerceToSpellGroupId, createCounteractStatistic, getSpellRankLabel, spellSlotGroupIdToNumber };
19 changes: 18 additions & 1 deletion src/module/item/weapon/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import type {
PhysicalSystemSource,
UsageDetails,
} from "@item/physical/index.ts";
import { ZeroToFour } from "@module/data.ts";
import type { SpellSlotGroupId } from "@item/spellcasting-entry/collection.ts";
import type { ZeroToFour } from "@module/data.ts";
import { DamageDieSize, DamageType } from "@system/damage/index.ts";
import type { WeaponTraitToggles } from "./trait-toggles.ts";
import type {
Expand Down Expand Up @@ -89,6 +90,12 @@ interface WeaponSystemSource extends Investable<PhysicalSystemSource> {
/** Doubly-embedded adjustments, attachments, talismans etc. */
subitems: PhysicalItemSource[];

/** If this is a staff, the number of charges and what spells is available on this staff */
staff: {
effect: string;
spells: StaffSpellData[];
} | null;

// Refers to custom damage, *not* property runes
property1: {
value: string;
Expand All @@ -103,6 +110,15 @@ interface WeaponSystemSource extends Investable<PhysicalSystemSource> {
selectedAmmoId: string | null;
}

interface StaffSpellData {
uuid: ItemUUID;
rank: SpellSlotGroupId;
/** The spell's name, used if the lookup fails */
name?: string;
/** The spell's image, used if the lookup fails */
img?: ImageFilePath;
}

interface WeaponTraitsSource extends PhysicalItemTraits<WeaponTrait> {
otherTags: OtherWeaponTag[];
toggles?: {
Expand Down Expand Up @@ -196,6 +212,7 @@ interface ComboWeaponMeleeUsage {
export type {
ComboWeaponMeleeUsage,
SpecificWeaponData,
StaffSpellData,
WeaponDamage,
WeaponFlags,
WeaponMaterialData,
Expand Down
45 changes: 44 additions & 1 deletion src/module/item/weapon/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ConsumablePF2e, MeleePF2e, ShieldPF2e } from "@item";
import { ItemProxyPF2e, PhysicalItemPF2e } from "@item";
import { createActionRangeLabel } from "@item/ability/helpers.ts";
import type { ItemSourcePF2e, MeleeSource, RawItemChatData } from "@item/base/data/index.ts";
import type { ItemDescriptionData } from "@item/base/data/system.ts";
import { performLatePreparation } from "@item/helpers.ts";
import type { NPCAttackDamage } from "@item/melee/data.ts";
import type { NPCAttackTrait } from "@item/melee/types.ts";
Expand All @@ -17,7 +18,8 @@ import type { RangeData } from "@item/types.ts";
import type { StrikeRuleElement } from "@module/rules/rule-element/strike.ts";
import type { UserPF2e } from "@module/user/document.ts";
import { DamageCategorization } from "@system/damage/helpers.ts";
import { ErrorPF2e, objectHasKey, setHasElement, sluggify, tupleHasValue } from "@util";
import { ErrorPF2e, objectHasKey, ordinalString, setHasElement, sluggify, tupleHasValue } from "@util";
import { UUIDUtils } from "@util/uuid.ts";
import * as R from "remeda";
import type { WeaponDamage, WeaponFlags, WeaponSource, WeaponSystemData } from "./data.ts";
import { processTwoHandTrait } from "./helpers.ts";
Expand Down Expand Up @@ -366,6 +368,13 @@ class WeaponPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Ph
const mandatoryMelee = !mandatoryRanged && traits.value.some((t) => /^thrown-{1,3}$/.test(t));
if (mandatoryMelee) this.system.range = null;

// Initialize staff spells if this weapon is a staff
if (traits.value.includes("staff")) {
this.system.staff = fu.mergeObject({ effect: "", spells: [] }, this.system.staff ?? {});
} else {
this.system.staff = null;
}

// Final sweep: remove any non-sensical trait that may throw off later automation
if (this.isMelee) {
traits.value = traits.value.filter((t) => !RANGED_ONLY_TRAITS.has(t) && !t.startsWith("volley"));
Expand Down Expand Up @@ -434,6 +443,34 @@ class WeaponPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Ph
processTwoHandTrait(this);
}

/** Get description with staff spells possibly included */
override async getDescription(): Promise<ItemDescriptionData> {
const description = await super.getDescription();
if (this.system.staff?.spells.length) {
const uuids = R.unique(this.system.staff.spells.map((s) => s.uuid));
const entriesByLevel = R.groupBy(
R.sortBy(this.system.staff.spells, (s) => s.rank),
(s) => (s.rank === "cantrips" ? 0 : s.rank),
);
const spellsByUUID = R.mapToObj(await UUIDUtils.fromUUIDs(uuids), (s) => [s.uuid, s]);
const append = await renderTemplate("systems/pf2e/templates/items/partials/staff-description-append.hbs", {
effect: this.system.staff.effect || game.i18n.localize("PF2E.Item.Weapon.Staff.DefaultEffect"),
spells: Object.values(entriesByLevel)
.map((group) => {
const rank = group[0].rank;
const spells = group.map((e) => spellsByUUID[e.uuid]).filter((s) => !!s);
const label =
rank === "cantrips" ? game.i18n.localize("PF2E.TraitCantrip") : ordinalString(rank);
return { label, spells: spells.map((s) => ({ link: s.link })) };
})
.filter((g) => g.spells.length),
});
description.value += `\n${append}`;
}

return description;
}

override async getChatData(
this: WeaponPF2e<ActorPF2e>,
htmlOptions: EnrichmentOptions = {},
Expand Down Expand Up @@ -745,6 +782,12 @@ class WeaponPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Ph

const traits = changed.system.traits ?? {};
if ("value" in traits && Array.isArray(traits.value)) {
// Clean up staff spells automatically if the staff trait is removed and the list is empty
const spells = changed.system.staff?.spells ?? this._source.system.staff?.spells;
if (!traits.value.includes("staff") && !spells?.length) {
changed.system.staff = null;
}

traits.value = traits.value.filter((t) => t in CONFIG.PF2E.weaponTraits);
}

Expand Down
Loading
Loading