From 570d71154e6dda7d86bf17b1a743bd88495a1b76 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Tue, 14 Jan 2025 05:42:51 -0500 Subject: [PATCH] Add support for registering spells to staves --- build/lib/compendium-pack.ts | 14 ++ src/module/item/base/document.ts | 7 +- src/module/item/base/sheet/sheet.ts | 18 ++- src/module/item/helpers.ts | 14 +- src/module/item/spell/document.ts | 16 +- src/module/item/spell/sheet.ts | 5 +- .../item/spellcasting-entry/collection.ts | 6 +- src/module/item/spellcasting-entry/helpers.ts | 10 +- src/module/item/weapon/data.ts | 19 ++- src/module/item/weapon/document.ts | 45 +++++- src/module/item/weapon/sheet.ts | 147 +++++++++++++++++- src/styles/item/_abc-sheet.scss | 69 -------- src/styles/item/_index.scss | 86 +++++++++- src/styles/item/_weapon-sheet.scss | 12 ++ static/lang/en.json | 6 + .../partials/staff-description-append.hbs | 13 ++ static/templates/items/weapon-details.hbs | 44 ++++++ 17 files changed, 430 insertions(+), 101 deletions(-) create mode 100644 static/templates/items/partials/staff-description-append.hbs diff --git a/build/lib/compendium-pack.ts b/build/lib/compendium-pack.ts index 2051d1bec98..cdb2fc044dd 100644 --- a/build/lib/compendium-pack.ts +++ b/build/lib/compendium-pack.ts @@ -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)) { @@ -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")) { diff --git a/src/module/item/base/document.ts b/src/module/item/base/document.ts index fefc2c51db9..f8e1c187ec5 100644 --- a/src/module/item/base/document.ts +++ b/src/module/item/base/document.ts @@ -30,7 +30,7 @@ import type { RawItemChatData, TraitChatData, } from "./data/index.ts"; -import type { ItemTrait } from "./data/system.ts"; +import type { ItemDescriptionData, ItemTrait } from "./data/system.ts"; import type { ItemSheetPF2e } from "./sheet/sheet.ts"; /** The basic `Item` subclass for the system */ @@ -430,6 +430,11 @@ class ItemPF2e extends Item /* Chat Card Data */ /* -------------------------------------------- */ + /** Retrieves base description data before enriching. May be overriden to prepend or append additional data */ + async getDescription(): Promise { + return { ...this.system.description }; + } + /** * Internal method that transforms data into something that can be used for chat. * Currently renders description text using enrichHTML. diff --git a/src/module/item/base/sheet/sheet.ts b/src/module/item/base/sheet/sheet.ts index 2c968740cec..5554bc6fd51 100644 --- a/src/module/item/base/sheet/sheet.ts +++ b/src/module/item/base/sheet/sheet.ts @@ -101,11 +101,12 @@ class ItemSheetPF2e extends ItemSheet extends ItemSheet(html, 'tagify-tags[name="system.traits.otherTags"]'), { maxTags: 6 }); // Handle select and input elements that show modified prepared values until focused - const modifiedPropertyFields = htmlQueryAll(html, "[data-property]"); + const modifiedPropertyFields = htmlQueryAll( + html, + "input[data-property], select[data-property]", + ); for (const input of modifiedPropertyFields) { const propertyPath = input.dataset.property ?? ""; const baseValue = @@ -502,6 +506,14 @@ class ItemSheetPF2e extends ItemSheet(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 && diff --git a/src/module/item/helpers.ts b/src/module/item/helpers.ts index 5ccb1383d87..08643cf26a7 100644 --- a/src/module/item/helpers.ts +++ b/src/module/item/helpers.ts @@ -79,14 +79,16 @@ class ItemChatData { } async #prepareDescription(): Promise> { - const { data, item } = this; + const { item } = this; const rollOptions = new Set( [item.actor?.getRollOptions(), item.getRollOptions("item")].flat().filter(R.isTruthy), ); + const description = await this.item.getDescription(); + const baseText = await (async (): Promise => { - const override = data.description?.override; - if (!override) return data.description.value; + const override = description?.override; + if (!override) return description.value; return override .flatMap((line) => { if (!line.predicate.test(rollOptions)) return []; @@ -115,7 +117,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 @@ -138,9 +140,7 @@ class ItemChatData { return { value: await TextEditor.enrichHTML(assembled, { ...this.htmlOptions, rollData }), - gm: game.user.isGM - ? await TextEditor.enrichHTML(data.description.gm, { ...this.htmlOptions, rollData }) - : "", + gm: game.user.isGM ? await TextEditor.enrichHTML(description.gm, { ...this.htmlOptions, rollData }) : "", }; } } diff --git a/src/module/item/spell/document.ts b/src/module/item/spell/document.ts index 6f1aa495f9c..df334ddae03 100644 --- a/src/module/item/spell/document.ts +++ b/src/module/item/spell/document.ts @@ -7,6 +7,7 @@ import type { ConsumablePF2e } from "@item"; import { ItemPF2e } from "@item"; import { processSanctification } from "@item/ability/helpers.ts"; import { ItemSourcePF2e, RawItemChatData } from "@item/base/data/index.ts"; +import { ItemDescriptionData } from "@item/base/data/system.ts"; import { SpellSlotGroupId } from "@item/spellcasting-entry/collection.ts"; import { spellSlotGroupIdToNumber } from "@item/spellcasting-entry/helpers.ts"; import { BaseSpellcastingEntry } from "@item/spellcasting-entry/types.ts"; @@ -835,6 +836,13 @@ class SpellPF2e extends Ite return ChatMessagePF2e.create(messageSource, { renderSheet: false }); } + override async getDescription(): Promise { + const description = await super.getDescription(); + const prepend = await createDescriptionPrepend(this, { includeTraditions: false }); + description.value = `${prepend}\n${description.value}`; + return description; + } + override async getChatData( this: SpellPF2e, htmlOptions: EnrichmentOptionsPF2e = {}, @@ -864,13 +872,6 @@ class SpellPF2e extends Ite const systemData: SpellSystemData = this.system; - const description = await (async () => { - const options = { ...htmlOptions, rollData }; - const prepend = await createDescriptionPrepend(this, { includeTraditions: false }); - const description = await TextEditor.enrichHTML(this.description, options); - return `${prepend}\n${description}`; - })(); - const spellcasting = this.spellcasting; if (!spellcasting) { console.warn( @@ -945,7 +946,6 @@ class SpellPF2e extends Ite return this.processChatData(htmlOptions, { ...systemData, - description: { ...this.system.description, value: description }, isAttack: this.isAttack, isSave, check: this.isAttack && statisticChatData ? statisticChatData.check : undefined, diff --git a/src/module/item/spell/sheet.ts b/src/module/item/spell/sheet.ts index c413f2f916a..4a6a4f24959 100644 --- a/src/module/item/spell/sheet.ts +++ b/src/module/item/spell/sheet.ts @@ -19,7 +19,7 @@ import { } from "@util"; import { tagify } from "@util/tags.ts"; import * as R from "remeda"; -import { createDescriptionPrepend, createSpellRankLabel } from "./helpers.ts"; +import { createSpellRankLabel } from "./helpers.ts"; import type { EffectAreaShape, SpellDamageSource, @@ -65,9 +65,6 @@ export class SpellSheetPF2e extends ItemSheetPF2e { const sheetData = await super.getData(options); const spell = this.item; - const descriptionPrepend = await createDescriptionPrepend(spell, { includeTraditions: true }); - sheetData.enrichedContent.description = `${descriptionPrepend}${sheetData.enrichedContent.description}`; - const variants = spell.overlays.overrideVariants .map((variant) => ({ name: variant.name, diff --git a/src/module/item/spellcasting-entry/collection.ts b/src/module/item/spellcasting-entry/collection.ts index 8db43ecd38a..7abc3d2af16 100644 --- a/src/module/item/spellcasting-entry/collection.ts +++ b/src/module/item/spellcasting-entry/collection.ts @@ -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 extends Collection> { @@ -366,8 +366,8 @@ class SpellCollection extends Collection { /** 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; @@ -103,6 +110,15 @@ interface WeaponSystemSource extends Investable { 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 { otherTags: OtherWeaponTag[]; toggles?: { @@ -196,6 +212,7 @@ interface ComboWeaponMeleeUsage { export type { ComboWeaponMeleeUsage, SpecificWeaponData, + StaffSpellData, WeaponDamage, WeaponFlags, WeaponMaterialData, diff --git a/src/module/item/weapon/document.ts b/src/module/item/weapon/document.ts index e0a078ae520..42f9dc1b05b 100644 --- a/src/module/item/weapon/document.ts +++ b/src/module/item/weapon/document.ts @@ -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 type { NPCAttackDamage } from "@item/melee/data.ts"; import type { NPCAttackTrait } from "@item/melee/types.ts"; import type { PhysicalItemConstructionContext } from "@item/physical/document.ts"; @@ -16,7 +17,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"; @@ -365,6 +367,13 @@ class WeaponPF2e 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")); @@ -433,6 +442,34 @@ class WeaponPF2e extends Ph processTwoHandTrait(this); } + /** Get description with staff spells possibly included */ + override async getDescription(): Promise { + 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, htmlOptions: EnrichmentOptions = {}, @@ -746,6 +783,12 @@ class WeaponPF2e 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); } diff --git a/src/module/item/weapon/sheet.ts b/src/module/item/weapon/sheet.ts index a72e1229078..3bdc7fe01b0 100644 --- a/src/module/item/weapon/sheet.ts +++ b/src/module/item/weapon/sheet.ts @@ -1,4 +1,5 @@ import { AutomaticBonusProgression as ABP } from "@actor/character/automatic-bonus-progression.ts"; +import { ItemPF2e } from "@item"; import { ItemSheetOptions } from "@item/base/sheet/sheet.ts"; import { MATERIAL_DATA, @@ -8,14 +9,32 @@ import { RUNE_DATA, getPropertyRuneSlots, } from "@item/physical/index.ts"; +import { SpellSlotGroupId } from "@item/spellcasting-entry/collection.ts"; +import { coerceToSpellGroupId, getSpellRankLabel } from "@item/spellcasting-entry/helpers.ts"; import { SheetOptions, createSheetTags } from "@module/sheet/helpers.ts"; -import { ErrorPF2e, htmlQueryAll, objectHasKey, setHasElement, sortStringRecord, tupleHasValue } from "@util"; +import { + ErrorPF2e, + htmlClosest, + htmlQueryAll, + objectHasKey, + setHasElement, + sortStringRecord, + tupleHasValue, +} from "@util"; +import { UUIDUtils } from "@util/uuid.ts"; import * as R from "remeda"; -import { ComboWeaponMeleeUsage, SpecificWeaponData, WeaponPersistentDamage } from "./data.ts"; +import type { ComboWeaponMeleeUsage, SpecificWeaponData, StaffSpellData, WeaponPersistentDamage } from "./data.ts"; import type { WeaponPF2e } from "./document.ts"; import { MANDATORY_RANGED_GROUPS, WEAPON_RANGES } from "./values.ts"; export class WeaponSheetPF2e extends PhysicalItemSheetPF2e { + static override get defaultOptions(): ItemSheetOptions { + return { + ...super.defaultOptions, + dragDrop: [{ dropSelector: ".staff-spells" }], + }; + } + protected override get validTraits(): Record { return CONFIG.PF2E.weaponTraits; } @@ -117,6 +136,41 @@ export class WeaponSheetPF2e extends PhysicalItemSheetPF2e { weaponMAP: CONFIG.PF2E.weaponMAP, weaponRanges, weaponReload: CONFIG.PF2E.weaponReload, + staff: await this.#prepareStaffSpells(), + }; + } + + async #prepareStaffSpells(): Promise { + const item = this.item; + const staff = item.system.staff; + if (!staff) return null; + + const items = await UUIDUtils.fromUUIDs(R.unique(staff.spells.map((s) => s.uuid))); + const itemsByUuid = R.mapToObj(items, (i) => [i.uuid, i]); + + const allSpellGroups: SpellSlotGroupId[] = ["cantrips", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + return { + defaultEffect: game.i18n.localize("PF2E.Item.Weapon.Staff.DefaultEffect"), + effect: staff.effect, + spells: allSpellGroups.map((rank) => ({ + rank, + label: getSpellRankLabel(rank), + spells: + staff.spells + .filter((s) => s.rank === rank) + .map((data) => { + const spell = itemsByUuid[data.uuid]; + return { + img: spell?.img ?? data.img, + name: spell?.name ?? data.name, + uuid: data.uuid, + rank, + fromWorld: data.uuid.startsWith("Item."), + linked: !!spell, + }; + }) ?? [], + })), }; } @@ -157,6 +211,18 @@ export class WeaponSheetPF2e extends PhysicalItemSheetPF2e { } }); } + + const staffSpellRemoves = htmlQueryAll(html, "[data-action=remove-staff-spell]"); + for (const element of staffSpellRemoves) { + const uuid = htmlClosest(element, "[data-uuid]")?.dataset.uuid; + const rank = coerceToSpellGroupId(htmlClosest(element, "[data-rank]")?.dataset.rank); + element.addEventListener("click", () => { + const spells = this.item.system.staff?.spells.filter((s) => !(s.uuid === uuid && s.rank === rank)); + if (spells) { + this.item.update({ system: { staff: { spells } } }); + } + }); + } } protected override async _updateObject(event: Event, formData: Record): Promise { @@ -183,6 +249,61 @@ export class WeaponSheetPF2e extends PhysicalItemSheetPF2e { return super._updateObject(event, formData); } + + protected override async _onDrop(event: DragEvent): Promise { + if (!this.isEditable) return; + + const item = await (async (): Promise => { + try { + const dataString = event.dataTransfer?.getData("text/plain"); + const dropData = JSON.parse(dataString ?? ""); + return (await ItemPF2e.fromDropData(dropData)) ?? null; + } catch { + return null; + } + })(); + + // Handle dragging an item to a staff + const staffSpellsElement = htmlClosest(event.target, ".staff-spells"); + if (staffSpellsElement && item?.isOfType("spell")) { + const savedSpells = this.item._source.system.staff?.spells ?? []; + const rank = item.isCantrip ? "cantrips" : coerceToSpellGroupId(staffSpellsElement.dataset.rank); + if (savedSpells.some((s) => s.uuid === item.uuid && s.rank === rank)) { + ui.notifications.warn("Spell already exists at this rank"); + return; + } else if (typeof rank === "number" && rank < item.baseRank) { + ui.notifications.warn( + game.i18n.format("PF2E.Item.Spell.Warning.InvalidRank", { + spell: item.name, + spellRank: getSpellRankLabel(item.baseRank), + targetRank: getSpellRankLabel(rank), + }), + ); + return; + } + + if (rank !== null) { + const spells: StaffSpellData[] = R.sortBy( + [...savedSpells, { img: item.img, name: item.name, rank, uuid: item.uuid }], + (s) => (s.rank === "cantrips" ? 0 : s.rank), + ); + await this.item.update({ system: { staff: { spells } } }); + } + + return; + } + + super._onDrop(event); + } + + /** Clean up staff data if this sheet was closed. Doing it here allows a window of recovery of accidental deletion */ + override close(options?: { force?: boolean | undefined }): Promise { + if (!this.item.system.traits.value.includes("staff") && this.item._source.system.staff) { + this.item.update({ "system.staff": null }); + } + + return super.close(options); + } } interface PropertyRuneSheetSlot { @@ -221,4 +342,26 @@ interface WeaponSheetData extends PhysicalItemSheetData { weaponMAP: typeof CONFIG.PF2E.weaponMAP; weaponRanges: Record; weaponReload: typeof CONFIG.PF2E.weaponReload; + staff: StaffSheetData | null; +} + +interface StaffSheetData { + defaultEffect: string; + effect: string; + spells: StaffSpellRankSheetData[]; +} + +interface StaffSpellRankSheetData { + rank: SpellSlotGroupId; + label: string; + spells: SpellBrief[]; +} + +interface SpellBrief { + uuid: ItemUUID; + rank: SpellSlotGroupId; + name: string; + img: ImageFilePath; + fromWorld: boolean; + linked: boolean; } diff --git a/src/styles/item/_abc-sheet.scss b/src/styles/item/_abc-sheet.scss index 6abf42dfec9..e69f89ff56b 100644 --- a/src/styles/item/_abc-sheet.scss +++ b/src/styles/item/_abc-sheet.scss @@ -1,72 +1,3 @@ -.item-ref-group { - ul.item-refs { - border: 1px solid var(--color-border-light-2); - border-radius: 3px; - padding: 0; - margin: 0; - - &.empty { - height: 1.75rem; - - > li { - font-style: italic; - font-weight: 500; - opacity: 0.75; - - .image-placeholder { - background: rgba(black, 0.1); - border: 1px solid var(--color-disabled); - border-radius: 2px; - box-sizing: border-box; - height: 1.625rem; - } - } - } - - > li { - align-items: center; - display: grid; - grid-template-columns: 1.625rem auto 2em 1em; - padding: var(--space-1); - - &:nth-of-type(even) { - background-color: rgba(120, 100, 82, 0.1); - } - - .name { - display: block; - height: 1em; - line-height: 1em; - margin-left: 0.25em; - - i.fa-globe { - padding: 0 var(--space-3); - } - } - - .level { - font-weight: 500; - height: 1.25em; - text-align: center; - } - - a.remove { - padding: 0 var(--space-2); - } - } - - ul { - grid-column: 1 / 5; - margin-top: 0; - padding-left: 0.5em; - - &:empty { - display: none; - } - } - } -} - .form-group > label a.small-button { padding-left: var(--space-4); font-size: 0.9em; diff --git a/src/styles/item/_index.scss b/src/styles/item/_index.scss index ef11af76a43..16cffaf7715 100644 --- a/src/styles/item/_index.scss +++ b/src/styles/item/_index.scss @@ -136,6 +136,17 @@ scrollbar-gutter: stable; } + .contenteditable-input { + background-color: rgba(255, 255, 255, 0.5); + border: 1px solid rgb(118, 118, 118); + color: var(--color-text-dark-input); + padding: 1px 2px; + &[placeholder]:empty::before { + content: attr(placeholder); + color: #555; + } + } + @import "sidebar"; } @@ -304,7 +315,7 @@ font-weight: 600; } - .form-group > label:first-of-type { + .form-group > label:first-of-type:not(.short) { flex-basis: 11em; } @@ -520,6 +531,79 @@ } } + .item-ref-group { + ul.item-refs { + border: 1px solid var(--color-border-light-2); + border-radius: 3px; + padding: 0; + margin: 0; + + &.empty { + height: 1.75rem; + + > li { + font-style: italic; + font-weight: 500; + opacity: 0.75; + + .image-placeholder { + background: rgba(black, 0.1); + border: 1px solid var(--color-disabled); + border-radius: 2px; + box-sizing: border-box; + height: 1.625rem; + } + } + } + + > li { + align-items: center; + display: grid; + grid-template: "img name level controls" / 1.625rem auto 2em 1em; + padding: var(--space-1); + + &:nth-of-type(even) { + background-color: rgba(120, 100, 82, 0.1); + } + + .name { + display: block; + height: 1em; + line-height: 1em; + margin-left: 0.25em; + + i.fa-solid { + padding: 0 var(--space-3); + + i.fa-solid { + padding-left: 0; + } + } + } + + .level { + font-weight: 500; + height: 1.25em; + text-align: center; + } + + a.remove { + grid-area: controls; + padding: 0 var(--space-2); + } + } + + ul { + grid-column: 1 / 5; + margin-top: 0; + padding-left: 0.5em; + + &:empty { + display: none; + } + } + } + } + @import "abc-sheet"; @import "activations"; @import "mystification"; diff --git a/src/styles/item/_weapon-sheet.scss b/src/styles/item/_weapon-sheet.scss index 6c5cf6165b4..88910a17167 100644 --- a/src/styles/item/_weapon-sheet.scss +++ b/src/styles/item/_weapon-sheet.scss @@ -1,3 +1,15 @@ .precious-material select { width: 18em; } + +.staff { + .item-ref-group { + margin-top: 0; + } + + .staff-spells { + h3 { + padding-top: var(--space-4); + } + } +} diff --git a/static/lang/en.json b/static/lang/en.json index f74f4921280..8e83a6ce80e 100644 --- a/static/lang/en.json +++ b/static/lang/en.json @@ -1762,9 +1762,11 @@ }, "Activation": { "Add": "Add Activation", + "Activate": "Activate", "Cast": "Cast a Spell", "Command": "command", "CommandSheetLabel": "Command", + "Effect": "Effect", "Envision": "envision", "EnvisionSheetLabel": "Envision", "Interact": "Interact", @@ -2721,6 +2723,10 @@ "Hint": "Marking this weapon as a specific magic weapon indicates that it does more than what its material composition and runes allow. The name, level, rarity, and price will no longer be overridden from precious material or runes at the time of marking. Those material and runes will, however, serve as a baseline for level, rarity, and price adjustments from later upgrades.", "Label": "Specific Magic Weapon" }, + "Staff": { + "Label": "Staff Spells", + "DefaultEffect": "You expend a number of charges from the item to cast a spell from its list." + }, "ThrownUsage": { "Label": "Thrown Usage" }, diff --git a/static/templates/items/partials/staff-description-append.hbs b/static/templates/items/partials/staff-description-append.hbs new file mode 100644 index 00000000000..cfb1175eb5c --- /dev/null +++ b/static/templates/items/partials/staff-description-append.hbs @@ -0,0 +1,13 @@ +
+

{{localize "PF2E.Item.Activation.Activate"}} {{localize "PF2E.Item.Activation.Cast"}}

+

{{localize "PF2E.Item.Activation.Effect"}} {{effect}}

+
    + {{#each spells as |section|}} +
  • + {{section.label}} + {{#each section.spells as |spell|}} + {{{spell.link}}} + {{/each}} +
  • + {{/each}} +
diff --git a/static/templates/items/weapon-details.hbs b/static/templates/items/weapon-details.hbs index 65f88ebbe17..1019f64ce13 100644 --- a/static/templates/items/weapon-details.hbs +++ b/static/templates/items/weapon-details.hbs @@ -293,6 +293,50 @@ {{/if}} +{{#if staff}} +
+ {{localize "PF2E.Item.Weapon.Staff.Label"}} +
+ + {{localize "PF2E.Item.Activation.Cast"}} +
+
+ + {{staff.effect}} +
+
+ {{#each staff.spells as |section|}} +
+

{{localize section.label}}

+ +
+ {{/each}} +
+
+{{/if}} +