From 82182d27de7407e01e6434ac4d7cb2cdbb566503 Mon Sep 17 00:00:00 2001 From: Shark that walks like a man <106829671+stwlam@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:49:36 -0600 Subject: [PATCH] Prune and reorganize several areas of spell data (#11026) --- build/lib/extractor.ts | 5 - build/run-migration.ts | 4 +- src/module/actor/creature/helpers.ts | 1 - src/module/actor/sheet/helpers.ts | 2 +- .../apps/compendium-browser/tabs/spell.ts | 31 +- src/module/item/armor/document.ts | 14 +- src/module/item/physical/runes.ts | 270 +++++++------- src/module/item/spell/data.ts | 80 ++-- src/module/item/spell/document.ts | 169 ++++----- src/module/item/spell/helpers.ts | 15 + src/module/item/spell/sheet.ts | 261 ++++++------- src/module/item/spell/types.ts | 12 +- src/module/item/spell/values.ts | 4 +- src/module/item/weapon/document.ts | 8 +- .../605-catch-up-to-template-json.ts | 70 ++-- .../621-remove-config-spell-schools.ts | 10 +- .../migrations/626-update-spell-category.ts | 30 +- .../migrations/627-lowercase-spell-saves.ts | 20 +- .../migrations/638-spell-components.ts | 20 +- .../migrations/659-multiple-damage-rows.ts | 8 +- .../migrations/663-fix-spell-damage.ts | 3 +- .../migrations/674-stable-homebrew-tag-ids.ts | 26 +- .../migrations/703-spell-damage-structure.ts | 14 +- .../migrations/747-fixed-heightening.ts | 10 +- .../migrations/829-rm-ritual-entries.ts | 8 + .../migrations/846-spell-school-optional.ts | 9 +- .../882-spell-data-reorganization.ts | 270 ++++++++++++++ src/module/migration/migrations/index.ts | 1 + src/module/migration/runner/base.ts | 2 +- src/module/system/damage/types.ts | 3 +- src/scripts/config/index.ts | 15 - src/scripts/config/iwr.ts | 5 - src/scripts/config/traits.ts | 31 +- src/styles/item/_index.scss | 17 +- src/styles/item/_spell-sheet.scss | 46 ++- static/lang/en.json | 85 ++--- static/template.json | 37 +- static/templates/items/spell-details.hbs | 348 ++++++++---------- static/templates/items/spell-overlay.hbs | 28 +- static/templates/items/spell-sidebar.hbs | 19 - 40 files changed, 1090 insertions(+), 921 deletions(-) create mode 100644 src/module/item/spell/helpers.ts create mode 100644 src/module/migration/migrations/882-spell-data-reorganization.ts diff --git a/build/lib/extractor.ts b/build/lib/extractor.ts index 51b6542274e..385ae1e7838 100644 --- a/build/lib/extractor.ts +++ b/build/lib/extractor.ts @@ -555,11 +555,6 @@ class PackExtractor { if (!source.system.onlyLevel1) { delete (source.system as { onlyLevel1?: boolean }).onlyLevel1; } - } else if (source.type === "spell") { - const components: Record = source.system.components; - for (const [key, value] of Object.entries(components)) { - if (value === false) delete components[key]; - } } else if (source.type === "spellcastingEntry" && this.#lastActor?.type === "npc") { delete (source.system as { ability?: unknown }).ability; } diff --git a/build/run-migration.ts b/build/run-migration.ts index 670b360a4f0..3d3657fa57e 100644 --- a/build/run-migration.ts +++ b/build/run-migration.ts @@ -10,7 +10,6 @@ import { getFilesRecursively } from "./lib/helpers.ts"; import { MigrationBase } from "@module/migration/base.ts"; import { MigrationRunnerBase } from "@module/migration/runner/base.ts"; -import { Migration860RMGroup } from "@module/migration/migrations/860-rm-group.ts"; import { Migration862SpecificMagicArmor } from "@module/migration/migrations/862-specific-magic-armor.ts"; import { Migration863FixMisspelledOrganaizationsProperty } from "@module/migration/migrations/863-fix-misspelled-organaizations-property.ts"; import { Migration864RemoveWeaponMAP } from "@module/migration/migrations/864-rm-weapon-map.ts"; @@ -26,6 +25,7 @@ import { Migration876FeatLevelTaken } from "@module/migration/migrations/876-fea import { Migration877PublicationData } from "@module/migration/migrations/877-publication-data.ts"; import { Migration878TakeABreather } from "@module/migration/migrations/878-take-a-breather.ts"; import { Migration879DeviseAStratagemAndFriends } from "@module/migration/migrations/879-devise-a-stratagem-and-friends.ts"; +import { Migration882SpellDataReorganization } from "@module/migration/migrations/882-spell-data-reorganization.ts"; // ^^^ don't let your IDE use the index in these imports. you need to specify the full path ^^^ const { window } = new JSDOM(); @@ -35,7 +35,6 @@ globalThis.HTMLParagraphElement = window.HTMLParagraphElement; globalThis.Text = window.Text; const migrations: MigrationBase[] = [ - new Migration860RMGroup(), new Migration862SpecificMagicArmor(), new Migration863FixMisspelledOrganaizationsProperty(), new Migration864RemoveWeaponMAP(), @@ -51,6 +50,7 @@ const migrations: MigrationBase[] = [ new Migration877PublicationData(), new Migration878TakeABreather(), new Migration879DeviseAStratagemAndFriends(), + new Migration882SpellDataReorganization(), ]; global.deepClone = (original: T): T => { diff --git a/src/module/actor/creature/helpers.ts b/src/module/actor/creature/helpers.ts index 75e8f40d672..fbe0bd73b9a 100644 --- a/src/module/actor/creature/helpers.ts +++ b/src/module/actor/creature/helpers.ts @@ -92,7 +92,6 @@ function setImmunitiesFromTraits(actor: CreaturePF2e): void { "drained", "fatigued", "healing", - "necromancy", "nonlethal-attacks", "paralyzed", "poison", diff --git a/src/module/actor/sheet/helpers.ts b/src/module/actor/sheet/helpers.ts index 5fffce9191d..0918a27c8f8 100644 --- a/src/module/actor/sheet/helpers.ts +++ b/src/module/actor/sheet/helpers.ts @@ -15,7 +15,7 @@ function onClickCreateSpell(actor: ActorPF2e, data: Record): vo rank > 0 ? [ game.i18n.format("PF2E.Item.Spell.Rank.Ordinal", { rank: ordinalString(rank) }), - game.i18n.localize(data.location === "rituals" ? "PF2E.SpellCategoryRitual" : "PF2E.SpellLabel"), + game.i18n.localize(data.location === "rituals" ? "PF2E.Item.Spell.Ritual.Label" : "TYPES.Item.spell"), ] : [null, game.i18n.localize("PF2E.TraitCantrip")]; const source = { diff --git a/src/module/apps/compendium-browser/tabs/spell.ts b/src/module/apps/compendium-browser/tabs/spell.ts index b47ad11cc18..46dfc852afb 100644 --- a/src/module/apps/compendium-browser/tabs/spell.ts +++ b/src/module/apps/compendium-browser/tabs/spell.ts @@ -11,19 +11,7 @@ export class CompendiumBrowserSpellTab extends CompendiumBrowserTab { /* MiniSearch */ override searchFields = ["name"]; - override storeFields = [ - "type", - "name", - "img", - "uuid", - "level", - "time", - "category", - "traditions", - "traits", - "rarity", - "source", - ]; + override storeFields = ["type", "name", "img", "uuid", "level", "time", "traditions", "traits", "rarity", "source"]; constructor(browser: CompendiumBrowser) { super(browser); @@ -41,11 +29,11 @@ export class CompendiumBrowserSpellTab extends CompendiumBrowserTab { const indexFields = [ "img", "system.level.value", - "system.category.value", - "system.traditions.value", + "system.traits.traditions", "system.time", "system.traits", "system.publication", + "system.ritual", "system.source", ]; @@ -62,10 +50,6 @@ export class CompendiumBrowserSpellTab extends CompendiumBrowserTab { ); continue; } - // Set category of cantrips to "cantrip" until migration can be done - if (spellData.system.traits.value.includes("cantrip")) { - spellData.system.category.value = "cantrip"; - } // recording casting times let time = spellData.system.time.value; @@ -101,8 +85,7 @@ export class CompendiumBrowserSpellTab extends CompendiumBrowserTab { uuid: `Compendium.${pack.collection}.${spellData._id}`, level: spellData.system.level.value, time: spellData.system.time, - category: spellData.system.category.value, - traditions: spellData.system.traditions.value, + traditions: spellData.system.traits.traditions, traits: spellData.system.traits.value.map((t: string) => t.replace(/^hb_/, "")), rarity: spellData.system.traits.rarity, source: sourceSlug, @@ -114,8 +97,6 @@ export class CompendiumBrowserSpellTab extends CompendiumBrowserTab { this.indexData = spells; // Filters - this.filterData.checkboxes.category.options = this.generateCheckboxOptions(CONFIG.PF2E.spellCategories); - this.filterData.checkboxes.category.options.cantrip = { label: "PF2E.TraitCantrip", selected: false }; this.filterData.checkboxes.traditions.options = this.generateCheckboxOptions(CONFIG.PF2E.magicTraditions); // Special case for spell ranks for (let rank = 1; rank <= 10; rank++) { @@ -151,10 +132,6 @@ export class CompendiumBrowserSpellTab extends CompendiumBrowserTab { if (selects.timefilter.selected) { if (!(selects.timefilter.selected === entry.time.value)) return false; } - // Category - if (checkboxes.category.selected.length) { - if (!checkboxes.category.selected.includes(entry.category)) return false; - } // Traditions if (checkboxes.traditions.selected.length) { if (!this.arrayIncludes(checkboxes.traditions.selected, entry.traditions)) return false; diff --git a/src/module/item/armor/document.ts b/src/module/item/armor/document.ts index a73ff95b0ee..741c9a5777f 100644 --- a/src/module/item/armor/document.ts +++ b/src/module/item/armor/document.ts @@ -126,13 +126,17 @@ class ArmorPF2e extends Phy this.prepareRunes(); // Add traits from fundamental runes + const abpEnabled = ABP.isEnabled(this.actor); const baseTraits = this.system.traits.value; - const fromRunes: ("invested" | "abjuration")[] = - this.system.runes.potency || this.system.runes.resilient ? ["invested", "abjuration"] : []; + const investedTrait = + this.system.runes.potency || + this.system.runes.resilient || + (abpEnabled && this.system.runes.property.length > 0) + ? "invested" + : null; const hasTraditionTraits = baseTraits.some((t) => setHasElement(MAGIC_TRADITIONS, t)); - const magicTraits: "magical"[] = fromRunes.length > 0 && !hasTraditionTraits ? ["magical"] : []; - - this.system.traits.value = R.uniq([baseTraits, fromRunes, magicTraits].flat()).sort(); + const magicTrait = investedTrait && !hasTraditionTraits ? "magical" : null; + this.system.traits.value = R.uniq(R.compact([...baseTraits, investedTrait, magicTrait]).sort()); } override prepareDerivedData(): void { diff --git a/src/module/item/physical/runes.ts b/src/module/item/physical/runes.ts index 2cf89cb0217..381cf87f2d4 100644 --- a/src/module/item/physical/runes.ts +++ b/src/module/item/physical/runes.ts @@ -174,7 +174,7 @@ const FUNDAMENTAL_ARMOR_RUNE_DATA: FundamentalArmorRuneData = { level: 5, price: 160, rarity: "common", - traits: ["abjuration"], + traits: [], }, 2: { name: "PF2E.ArmorPotencyRune2", @@ -182,7 +182,7 @@ const FUNDAMENTAL_ARMOR_RUNE_DATA: FundamentalArmorRuneData = { level: 11, price: 1060, rarity: "common", - traits: ["abjuration"], + traits: [], }, 3: { name: "PF2E.ArmorPotencyRune3", @@ -190,7 +190,7 @@ const FUNDAMENTAL_ARMOR_RUNE_DATA: FundamentalArmorRuneData = { level: 18, price: 20_560, rarity: "common", - traits: ["abjuration"], + traits: [], }, 4: { name: "PF2E.ArmorPotencyRune4", @@ -198,7 +198,7 @@ const FUNDAMENTAL_ARMOR_RUNE_DATA: FundamentalArmorRuneData = { level: 18, price: 20_560, rarity: "common", - traits: ["abjuration"], + traits: [], }, }, resilient: { @@ -209,7 +209,7 @@ const FUNDAMENTAL_ARMOR_RUNE_DATA: FundamentalArmorRuneData = { price: 340, rarity: "common", slug: "resilient", - traits: ["abjuration"], + traits: [], }, 2: { name: "PF2E.ArmorGreaterResilientRune", @@ -217,7 +217,7 @@ const FUNDAMENTAL_ARMOR_RUNE_DATA: FundamentalArmorRuneData = { price: 3440, rarity: "common", slug: "greaterResilient", - traits: ["abjuration"], + traits: [], }, 3: { name: "PF2E.ArmorMajorResilientRune", @@ -225,7 +225,7 @@ const FUNDAMENTAL_ARMOR_RUNE_DATA: FundamentalArmorRuneData = { price: 49_440, rarity: "common", slug: "majorResilient", - traits: ["abjuration"], + traits: [], }, }, }; @@ -248,7 +248,7 @@ const FUNDAMENTAL_WEAPON_RUNE_DATA: FundamentalWeaponRuneData = { level: 2, price: 35, rarity: "common", - traits: ["evocation"], + traits: [], }, 2: { name: "PF2E.WeaponPotencyRune2", @@ -256,7 +256,7 @@ const FUNDAMENTAL_WEAPON_RUNE_DATA: FundamentalWeaponRuneData = { level: 10, price: 935, rarity: "common", - traits: ["evocation"], + traits: [], }, 3: { name: "PF2E.WeaponPotencyRune3", @@ -264,7 +264,7 @@ const FUNDAMENTAL_WEAPON_RUNE_DATA: FundamentalWeaponRuneData = { level: 16, price: 8935, rarity: "common", - traits: ["evocation"], + traits: [], }, 4: { name: "PF2E.WeaponPotencyRune4", @@ -272,7 +272,7 @@ const FUNDAMENTAL_WEAPON_RUNE_DATA: FundamentalWeaponRuneData = { level: 16, price: 8935, rarity: "common", - traits: ["evocation"], + traits: [], }, }, // https://2e.aonprd.com/Equipment.aspx?Category=23&Subcategory=25 @@ -284,7 +284,7 @@ const FUNDAMENTAL_WEAPON_RUNE_DATA: FundamentalWeaponRuneData = { price: 65, rarity: "common", slug: "striking", - traits: ["evocation"], + traits: [], }, 2: { name: "PF2E.Item.Weapon.Rune.Striking.Greater", @@ -292,7 +292,7 @@ const FUNDAMENTAL_WEAPON_RUNE_DATA: FundamentalWeaponRuneData = { price: 1065, rarity: "common", slug: "greaterStriking", - traits: ["evocation"], + traits: [], }, 3: { name: "PF2E.Item.Weapon.Rune.Striking.Major", @@ -300,7 +300,7 @@ const FUNDAMENTAL_WEAPON_RUNE_DATA: FundamentalWeaponRuneData = { price: 31_065, rarity: "common", slug: "majorStriking", - traits: ["evocation"], + traits: [], }, }, }; @@ -345,7 +345,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 420, rarity: "common", slug: "acidResistant", - traits: ["abjuration", "magical"], + traits: ["magical"], }, advancing: { name: "PF2E.ArmorPropertyRuneAdvancing", @@ -353,7 +353,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 625, rarity: "common", slug: "advancing", - traits: ["magical", "necromancy"], + traits: ["magical"], }, aimAiding: { name: "PF2E.ArmorPropertyRuneAimAiding", @@ -361,7 +361,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 225, rarity: "common", slug: "aimAiding", - traits: ["magical", "transmutation"], + traits: ["magical"], }, antimagic: { name: "PF2E.ArmorPropertyRuneAntimagic", @@ -369,7 +369,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 6500, rarity: "uncommon", slug: "antimagic", - traits: ["abjuration", "magical"], + traits: ["magical"], }, assisting: { name: "PF2E.ArmorPropertyRuneAssisting", @@ -377,7 +377,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 125, rarity: "common", slug: "assisting", - traits: ["magical", "transmutation"], + traits: ["magical"], }, bitter: { name: "PF2E.ArmorPropertyRuneBitter", @@ -385,7 +385,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 135, rarity: "uncommon", slug: "bitter", - traits: ["magical", "poison", "transmutation"], + traits: ["magical", "poison"], }, coldResistant: { name: "PF2E.ArmorPropertyRuneColdResistant", @@ -393,7 +393,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 420, rarity: "common", slug: "coldResistant", - traits: ["abjuration", "magical"], + traits: ["magical"], }, deathless: { name: "PF2E.ArmorPropertyRuneDeathless", @@ -401,7 +401,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 330, rarity: "uncommon", slug: "deathless", - traits: ["healing", "magical", "necromancy"], + traits: ["healing", "magical"], }, electricityResistant: { name: "PF2E.ArmorPropertyRuneElectricityResistant", @@ -409,7 +409,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 420, rarity: "common", slug: "electricityResistant", - traits: ["abjuration", "magical"], + traits: ["magical"], }, energyAdaptive: { name: "PF2E.ArmorPropertyRuneEnergyAdaptive", @@ -417,7 +417,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 225, rarity: "common", slug: "energyAdaptive", - traits: ["magical", "transmutation"], + traits: ["magical"], }, ethereal: { name: "PF2E.ArmorPropertyRuneEthereal", @@ -425,7 +425,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 13_500, rarity: "common", slug: "ethereal", - traits: ["conjuration", "magical"], + traits: ["magical"], }, fireResistant: { name: "PF2E.ArmorPropertyRuneFireResistant", @@ -433,7 +433,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 420, rarity: "common", slug: "fireResistant", - traits: ["abjuration", "magical"], + traits: ["magical"], }, fortification: { name: "PF2E.ArmorPropertyRuneFortification", @@ -441,7 +441,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 2000, rarity: "common", slug: "fortification", - traits: ["abjuration", "magical"], + traits: ["magical"], }, glamered: { name: "PF2E.ArmorPropertyRuneGlamered", @@ -457,7 +457,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 450, rarity: "common", slug: "gliding", - traits: ["magical", "transmutation"], + traits: ["magical"], }, greaterAcidResistant: { name: "PF2E.ArmorPropertyRuneGreaterAcidResistant", @@ -465,7 +465,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1650, rarity: "common", slug: "greaterAcidResistant", - traits: ["abjuration", "magical"], + traits: ["magical"], }, greaterAdvancing: { name: "PF2E.ArmorPropertyRuneGreaterAdvancing", @@ -473,7 +473,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 8000, rarity: "common", slug: "greaterAdvancing", - traits: ["magical", "necromancy"], + traits: ["magical"], }, greaterColdResistant: { name: "PF2E.ArmorPropertyRuneGreaterColdResistant", @@ -481,7 +481,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1650, rarity: "common", slug: "greaterColdResistant", - traits: ["abjuration", "magical"], + traits: ["magical"], }, greaterDread: { name: "PF2E.ArmorPropertyRuneGreaterDread", @@ -489,7 +489,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 21_000, rarity: "uncommon", slug: "greaterDread", - traits: ["emotion", "enchantment", "fear", "magical", "mental", "visual"], + traits: ["emotion", "fear", "magical", "mental", "visual"], }, greaterElectricityResistant: { name: "PF2E.ArmorPropertyRuneGreaterElectricityResistant", @@ -497,7 +497,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1650, rarity: "common", slug: "greaterElectricityResistant", - traits: ["abjuration", "magical"], + traits: ["magical"], }, greaterFireResistant: { name: "PF2E.ArmorPropertyRuneGreaterFireResistant", @@ -505,7 +505,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1650, rarity: "common", slug: "greaterFireResistant", - traits: ["abjuration", "magical"], + traits: ["magical"], }, greaterFortification: { name: "PF2E.ArmorPropertyRuneGreaterFortification", @@ -513,7 +513,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 24_000, rarity: "common", slug: "greaterFortification", - traits: ["abjuration", "magical"], + traits: ["magical"], }, greaterInvisibility: { name: "PF2E.ArmorPropertyRuneGreaterInvisibility", @@ -529,7 +529,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1200, rarity: "common", slug: "greaterReady", - traits: ["evocation", "magical"], + traits: ["magical"], }, greaterShadow: { name: "PF2E.ArmorPropertyRuneGreaterShadow", @@ -537,7 +537,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 650, rarity: "common", slug: "greaterShadow", - traits: ["magical", "transmutation"], + traits: ["magical"], }, greaterSlick: { name: "PF2E.ArmorPropertyRuneGreaterSlick", @@ -545,7 +545,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 450, rarity: "common", slug: "greaterSlick", - traits: ["magical", "transmutation"], + traits: ["magical"], }, greaterStanching: { name: "PF2E.ArmorPropertyRuneGreaterStanching", @@ -553,7 +553,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 600, rarity: "uncommon", slug: "greaterStanching", - traits: ["magical", "necromancy"], + traits: ["magical"], }, greaterQuenching: { name: "PF2E.ArmorPropertyRuneGreaterQuenching", @@ -561,7 +561,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1000, rarity: "common", slug: "greaterQuenching", - traits: ["abjuration", "magical"], + traits: ["magical"], }, greaterSwallowSpike: { name: "PF2E.ArmorPropertyRuneGreaterSwallowSpike", @@ -569,7 +569,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1750, rarity: "common", slug: "greaterSwallowSpike", - traits: ["magical", "transmutation"], + traits: ["magical"], }, greaterWinged: { name: "PF2E.ArmorPropertyRuneGreaterWinged", @@ -577,7 +577,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 35_000, rarity: "common", slug: "greaterWinged", - traits: ["magical", "transmutation"], + traits: ["magical"], }, immovable: { name: "PF2E.ArmorPropertyRuneImmovable", @@ -585,7 +585,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1800, rarity: "uncommon", slug: "immovable", - traits: ["magical", "transmutation"], + traits: ["magical"], }, implacable: { name: "PF2E.ArmorPropertyRuneImplacable", @@ -593,7 +593,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1200, rarity: "uncommon", slug: "implacable", - traits: ["magical", "transmutation"], + traits: ["magical"], }, invisibility: { name: "PF2E.ArmorPropertyRuneInvisibility", @@ -609,7 +609,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 225, rarity: "uncommon", slug: "lesserDread", - traits: ["emotion", "enchantment", "fear", "magical", "mental", "visual"], + traits: ["emotion", "fear", "magical", "mental", "visual"], }, magnetizing: { name: "PF2E.ArmorPropertyRuneMagnetizing", @@ -617,7 +617,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 900, rarity: "common", slug: "magnetizing", - traits: ["evocation", "magical"], + traits: ["magical"], }, majorQuenching: { name: "PF2E.ArmorPropertyRuneMajorQuenching", @@ -625,7 +625,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 4500, rarity: "common", slug: "majorQuenching", - traits: ["abjuration", "magical"], + traits: ["magical"], }, majorShadow: { name: "PF2E.ArmorPropertyRuneMajorShadow", @@ -633,7 +633,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 14_000, rarity: "common", slug: "majorShadow", - traits: ["magical", "transmutation"], + traits: ["magical"], }, majorSlick: { name: "PF2E.ArmorPropertyRuneMajorSlick", @@ -641,7 +641,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 9000, rarity: "common", slug: "majorSlick", - traits: ["magical", "transmutation"], + traits: ["magical"], }, majorStanching: { name: "PF2E.ArmorPropertyRuneMajorStanching", @@ -649,7 +649,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 2500, rarity: "uncommon", slug: "majorStanching", - traits: ["magical", "necromancy"], + traits: ["magical"], }, majorSwallowSpike: { name: "PF2E.ArmorPropertyRuneMajorSwallowSpike", @@ -657,7 +657,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 19_250, rarity: "common", slug: "majorSwallowSpike", - traits: ["magical", "transmutation"], + traits: ["magical"], }, malleable: { name: "PF2E.ArmorPropertyRuneMalleable", @@ -681,7 +681,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 1800, rarity: "uncommon", slug: "moderateDread", - traits: ["emotion", "enchantment", "fear", "magical", "mental", "visual"], + traits: ["emotion", "fear", "magical", "mental", "visual"], }, portable: { name: "PF2E.ArmorPropertyRunePortable", @@ -689,7 +689,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 660, rarity: "common", slug: "portable", - traits: ["magical", "transmutation"], + traits: ["magical"], }, quenching: { name: "PF2E.ArmorPropertyRuneQuenching", @@ -697,7 +697,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 250, rarity: "common", slug: "quenching", - traits: ["abjuration", "magical"], + traits: ["magical"], }, ready: { name: "PF2E.ArmorPropertyRuneReady", @@ -705,7 +705,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 200, rarity: "common", slug: "ready", - traits: ["evocation", "magical"], + traits: ["magical"], }, rockBraced: { name: "PF2E.ArmorPropertyRuneRockBraced", @@ -713,7 +713,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 3000, rarity: "rare", slug: "rockBraced", - traits: ["abjuration", "dwarf", "magical", "saggorak"], + traits: ["dwarf", "magical", "saggorak"], }, shadow: { name: "PF2E.ArmorPropertyRuneShadow", @@ -721,7 +721,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 55, rarity: "common", slug: "shadow", - traits: ["magical", "transmutation"], + traits: ["magical"], }, sinisterKnight: { name: "PF2E.ArmorPropertyRuneSinisterKnight", @@ -729,7 +729,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 500, rarity: "uncommon", slug: "sinisterKnight", - traits: ["abjuration", "illusion", "magical"], + traits: ["illusion", "magical"], }, slick: { name: "PF2E.ArmorPropertyRuneSlick", @@ -737,7 +737,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 45, rarity: "common", slug: "slick", - traits: ["magical", "transmutation"], + traits: ["magical"], }, soaring: { name: "PF2E.ArmorPropertyRuneSoaring", @@ -745,7 +745,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 3750, rarity: "common", slug: "soaring", - traits: ["abjuration", "magical"], + traits: ["magical"], }, stanching: { name: "PF2E.ArmorPropertyRuneStanching", @@ -753,7 +753,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 130, rarity: "uncommon", slug: "stanching", - traits: ["magical", "necromancy"], + traits: ["magical"], }, swallowSpike: { name: "PF2E.ArmorPropertyRuneSwallowSpike", @@ -761,7 +761,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 200, rarity: "common", slug: "swallowSpike", - traits: ["magical", "transmutation"], + traits: ["magical"], }, trueQuenching: { name: "PF2E.ArmorPropertyRuneTrueQuenching", @@ -769,7 +769,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 24_000, rarity: "common", slug: "trueQuenching", - traits: ["abjuration", "magical"], + traits: ["magical"], }, trueStanching: { name: "PF2E.ArmorPropertyRuneTrueStanching", @@ -777,7 +777,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 12_500, rarity: "uncommon", slug: "trueStanching", - traits: ["magical", "necromancy"], + traits: ["magical"], }, winged: { name: "PF2E.ArmorPropertyRuneWinged", @@ -785,7 +785,7 @@ export const ARMOR_PROPERTY_RUNES: { [T in ArmorPropertyRuneType]: ArmorProperty price: 2500, rarity: "common", slug: "winged", - traits: ["magical", "transmutation"], + traits: ["magical"], }, }; @@ -813,7 +813,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 1400, rarity: "common", slug: "anarchic", - traits: ["chaotic", "evocation", "magical"], + traits: ["chaotic", "magical"], }, ancestralEchoing: { level: 15, @@ -821,7 +821,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 9500, rarity: "rare", slug: "ancestralEchoing", - traits: ["dwarf", "evocation", "magical", "saggorak"], + traits: ["dwarf", "magical", "saggorak"], }, anchoring: { damage: { @@ -838,7 +838,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 900, rarity: "uncommon", slug: "anchoring", - traits: ["abjuration", "magical"], + traits: ["magical"], }, ashen: { damage: { @@ -863,7 +863,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 700, rarity: "common", slug: "ashen", - traits: ["enchantment", "magical"], + traits: ["magical"], }, authorized: { level: 3, @@ -871,7 +871,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 50, rarity: "common", slug: "authorized", - traits: ["abjuration", "magical"], + traits: ["magical"], }, axiomatic: { damage: { @@ -895,7 +895,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 1400, rarity: "common", slug: "axiomatic", - traits: ["evocation", "lawful", "magical"], + traits: ["lawful", "magical"], }, bane: { level: 4, @@ -903,7 +903,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 100, rarity: "uncommon", slug: "bane", - traits: ["divination", "magical"], + traits: ["magical"], }, bloodbane: { level: 8, @@ -911,7 +911,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 475, rarity: "uncommon", slug: "bloodbane", - traits: ["dwarf", "evocation", "magical"], + traits: ["dwarf", "magical"], }, bloodthirsty: { damage: { @@ -928,7 +928,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 8500, rarity: "uncommon", slug: "bloodthirsty", - traits: ["magical", "necromancy"], + traits: ["magical"], }, brilliant: { damage: { @@ -960,7 +960,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 2000, rarity: "common", slug: "brilliant", - traits: ["evocation", "magical"], + traits: ["magical"], }, called: { level: 7, @@ -968,7 +968,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 350, rarity: "common", slug: "called", - traits: ["conjuration", "magical"], + traits: ["magical"], }, coating: { level: 9, @@ -976,7 +976,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 700, rarity: "common", slug: "coating", - traits: ["conjuration", "extradimensional", "magical"], + traits: ["extradimensional", "magical"], }, conducting: { level: 7, @@ -984,7 +984,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 300, rarity: "common", slug: "conducting", - traits: ["evocation", "magical"], + traits: ["magical"], }, corrosive: { damage: { @@ -1002,7 +1002,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 500, rarity: "common", slug: "corrosive", - traits: ["acid", "conjuration", "magical"], + traits: ["acid", "magical"], }, crushing: { damage: { @@ -1019,7 +1019,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 50, rarity: "uncommon", slug: "crushing", - traits: ["magical", "necromancy"], + traits: ["magical"], }, cunning: { level: 5, @@ -1027,7 +1027,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 140, rarity: "common", slug: "cunning", - traits: ["divination", "magical"], + traits: ["magical"], }, dancing: { level: 13, @@ -1035,7 +1035,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 2700, rarity: "uncommon", slug: "dancing", - traits: ["evocation", "magical"], + traits: ["magical"], }, deathdrinking: { damage: { @@ -1063,7 +1063,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 360, rarity: "rare", slug: "deathdrinking", - traits: ["magical", "necromancy"], + traits: ["magical"], }, demolishing: { damage: { @@ -1082,7 +1082,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 225, rarity: "rare", slug: "demolishing", - traits: ["evocation", "magical"], + traits: ["magical"], }, disrupting: { damage: { @@ -1108,7 +1108,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 150, rarity: "common", slug: "disrupting", - traits: ["magical", "necromancy"], + traits: ["magical"], }, earthbinding: { level: 5, @@ -1116,7 +1116,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 125, rarity: "common", slug: "earthbinding", - traits: ["magical", "transmutation"], + traits: ["magical"], }, energizing: { level: 6, @@ -1124,7 +1124,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 250, rarity: "uncommon", slug: "energizing", - traits: ["abjuration", "magical"], + traits: ["magical"], }, extending: { level: 7, @@ -1132,7 +1132,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 700, rarity: "common", slug: "extending", - traits: ["magical", "transmutation"], + traits: ["magical"], }, fanged: { level: 2, @@ -1140,7 +1140,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 30, rarity: "uncommon", slug: "fanged", - traits: ["magical", "transmutation"], + traits: ["magical"], }, fearsome: { damage: { @@ -1157,7 +1157,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 160, rarity: "common", slug: "fearsome", - traits: ["emotion", "enchantment", "fear", "magical", "mental"], + traits: ["emotion", "fear", "magical", "mental"], }, flaming: { damage: { @@ -1177,7 +1177,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 500, rarity: "common", slug: "flaming", - traits: ["conjuration", "fire", "magical"], + traits: ["fire", "magical"], }, flurrying: { level: 7, @@ -1185,7 +1185,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 360, rarity: "common", slug: "flurrying", - traits: ["evocation", "magical"], + traits: ["magical"], }, frost: { damage: { @@ -1203,7 +1203,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 500, rarity: "common", slug: "frost", - traits: ["cold", "conjuration", "magical"], + traits: ["cold", "magical"], }, ghostTouch: { level: 4, @@ -1211,7 +1211,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 75, rarity: "common", slug: "ghostTouch", - traits: ["magical", "transmutation"], + traits: ["magical"], }, giantKilling: { damage: { @@ -1238,7 +1238,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 450, rarity: "rare", slug: "giantKilling", - traits: ["magical", "necromancy"], + traits: ["magical"], }, greaterAnchoring: { damage: { @@ -1260,7 +1260,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 22_000, rarity: "uncommon", slug: "greaterAnchoring", - traits: ["abjuration", "magical"], + traits: ["magical"], }, greaterAshen: { damage: { @@ -1285,7 +1285,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 9000, rarity: "common", slug: "greaterAshen", - traits: ["enchantment", "magical"], + traits: ["magical"], }, greaterBloodbane: { level: 13, @@ -1293,7 +1293,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 2800, rarity: "uncommon", slug: "greaterBloodbane", - traits: ["dwarf", "evocation", "magical"], + traits: ["dwarf", "magical"], }, greaterBrilliant: { damage: { @@ -1335,7 +1335,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 24_000, rarity: "common", slug: "greaterBrilliant", - traits: ["evocation", "magical"], + traits: ["magical"], }, greaterCorrosive: { damage: { @@ -1358,7 +1358,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 6500, rarity: "common", slug: "greaterCorrosive", - traits: ["acid", "conjuration", "magical"], + traits: ["acid", "magical"], }, greaterCrushing: { damage: { @@ -1375,7 +1375,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 650, rarity: "uncommon", slug: "greaterCrushing", - traits: ["magical", "necromancy"], + traits: ["magical"], }, greaterDisrupting: { damage: { @@ -1401,7 +1401,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 4300, rarity: "uncommon", slug: "greaterDisrupting", - traits: ["magical", "necromancy"], + traits: ["magical"], }, greaterExtending: { level: 13, @@ -1409,7 +1409,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 3000, rarity: "common", slug: "greaterExtending", - traits: ["magical", "transmutation"], + traits: ["magical"], }, greaterFanged: { level: 8, @@ -1417,7 +1417,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 425, rarity: "uncommon", slug: "greaterFanged", - traits: ["magical", "transmutation"], + traits: ["magical"], }, greaterFearsome: { damage: { @@ -1434,7 +1434,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 2000, rarity: "common", slug: "greaterFearsome", - traits: ["emotion", "enchantment", "fear", "magical", "mental"], + traits: ["emotion", "fear", "magical", "mental"], }, greaterFlaming: { damage: { @@ -1467,7 +1467,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 6500, rarity: "common", slug: "greaterFlaming", - traits: ["conjuration", "fire", "magical"], + traits: ["fire", "magical"], }, greaterFrost: { damage: { @@ -1491,7 +1491,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 6500, rarity: "common", slug: "greaterFrost", - traits: ["cold", "conjuration", "magical"], + traits: ["cold", "magical"], }, greaterGiantKilling: { damage: { @@ -1519,7 +1519,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 6000, rarity: "rare", slug: "greaterGiantKilling", - traits: ["magical", "necromancy"], + traits: ["magical"], }, greaterHauling: { level: 11, @@ -1527,7 +1527,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 1300, rarity: "uncommon", slug: "greaterHauling", - traits: ["evocation", "magical"], + traits: ["magical"], }, greaterImpactful: { damage: { @@ -1545,7 +1545,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 15_000, rarity: "common", slug: "greaterImpactful", - traits: ["evocation", "force", "magical"], + traits: ["force", "magical"], }, greaterRooting: { level: 11, @@ -1591,7 +1591,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 6500, rarity: "common", slug: "greaterShock", - traits: ["electricity", "evocation", "magical"], + traits: ["electricity", "magical"], }, greaterThundering: { damage: { @@ -1615,7 +1615,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 6500, rarity: "common", slug: "greaterThundering", - traits: ["evocation", "magical", "sonic"], + traits: ["magical", "sonic"], }, grievous: { damage: { @@ -1709,7 +1709,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 700, rarity: "common", slug: "grievous", - traits: ["enchantment", "magical"], + traits: ["magical"], }, hauling: { level: 6, @@ -1717,7 +1717,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 225, rarity: "uncommon", slug: "hauling", - traits: ["evocation", "magical"], + traits: ["magical"], }, holy: { damage: { @@ -1734,7 +1734,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 1400, rarity: "common", slug: "holy", - traits: ["evocation", "good", "magical"], + traits: ["good", "magical"], }, hopeful: { attack: { @@ -1751,7 +1751,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 1200, rarity: "uncommon", slug: "hopeful", - traits: ["enchantment", "magical"], + traits: ["magical"], }, hooked: { level: 5, @@ -1759,7 +1759,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 140, rarity: "rare", slug: "hooked", - traits: ["conjuration", "magical"], + traits: ["magical"], strikeAdjustments: [ { adjustWeapon: (weapon: WeaponPF2e | MeleePF2e): void => { @@ -1786,7 +1786,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 1000, rarity: "common", slug: "impactful", - traits: ["evocation", "force", "magical"], + traits: ["force", "magical"], }, impossible: { level: 20, @@ -1794,7 +1794,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 70_000, rarity: "common", slug: "impossible", - traits: ["conjuration", "magical"], + traits: ["magical"], strikeAdjustments: [ { // Double the base range increment @@ -1826,7 +1826,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 3000, rarity: "uncommon", slug: "keen", - traits: ["magical", "transmutation"], + traits: ["magical"], }, kinWarding: { level: 3, @@ -1834,7 +1834,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 52, rarity: "uncommon", slug: "kinWarding", - traits: ["abjuration", "dwarf", "magical"], + traits: ["dwarf", "magical"], }, majorFanged: { level: 15, @@ -1842,7 +1842,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 6000, rarity: "uncommon", slug: "majorFanged", - traits: ["magical", "transmutation"], + traits: ["magical"], }, majorRooting: { level: 15, @@ -1876,7 +1876,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 70, rarity: "common", slug: "merciful", - traits: ["abjuration", "magical", "mental"], + traits: ["magical", "mental"], }, pacifying: { level: 5, @@ -1884,7 +1884,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 150, rarity: "uncommon", slug: "pacifying", - traits: ["enchantment", "magical"], + traits: ["magical"], }, returning: { attack: { @@ -1897,7 +1897,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 55, rarity: "common", slug: "returning", - traits: ["evocation", "magical"], + traits: ["magical"], }, rooting: { level: 7, @@ -1925,7 +1925,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 1000, rarity: "uncommon", slug: "serrating", - traits: ["evocation", "magical"], + traits: ["magical"], }, shifting: { level: 6, @@ -1933,7 +1933,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 225, rarity: "common", slug: "shifting", - traits: ["magical", "transmutation"], + traits: ["magical"], }, shock: { damage: { @@ -1951,7 +1951,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 500, rarity: "common", slug: "shock", - traits: ["electricity", "conjuration", "magical"], + traits: ["electricity", "magical"], }, speed: { level: 16, @@ -1959,7 +1959,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 10_000, rarity: "rare", slug: "speed", - traits: ["magical", "transmutation"], + traits: ["magical"], }, spellStoring: { level: 13, @@ -1967,7 +1967,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 2700, rarity: "uncommon", slug: "spellStoring", - traits: ["abjuration", "magical"], + traits: ["magical"], }, swarming: { level: 9, @@ -1975,7 +1975,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 700, rarity: "common", slug: "swarming", - traits: ["conjuration", "magical"], + traits: ["magical"], }, thundering: { damage: { @@ -1993,7 +1993,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 500, rarity: "common", slug: "thundering", - traits: ["evocation", "magical", "sonic"], + traits: ["magical", "sonic"], }, trueRooting: { level: 19, @@ -2035,7 +2035,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 1400, rarity: "common", slug: "unholy", - traits: ["evil", "evocation", "magical"], + traits: ["evil", "magical"], }, vorpal: { level: 17, @@ -2043,7 +2043,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 15_000, rarity: "rare", slug: "vorpal", - traits: ["evocation", "magical"], + traits: ["magical"], }, wounding: { damage: { @@ -2054,7 +2054,7 @@ const WEAPON_PROPERTY_RUNES: { [T in WeaponPropertyRuneType]: WeaponPropertyRune price: 340, rarity: "common", slug: "wounding", - traits: ["magical", "necromancy"], + traits: ["magical"], }, }; diff --git a/src/module/item/spell/data.ts b/src/module/item/spell/data.ts index 36eaec29c21..8bd108daf0d 100644 --- a/src/module/item/spell/data.ts +++ b/src/module/item/spell/data.ts @@ -2,24 +2,14 @@ import { SaveType } from "@actor/types.ts"; import { BaseItemSourcePF2e, ItemSystemData, ItemSystemSource, ItemTraits } from "@item/base/data/system.ts"; import { OneToTen, ValueAndMax } from "@module/data.ts"; import { DamageCategoryUnique, DamageType, MaterialDamageEffect } from "@system/damage/index.ts"; -import { EffectAreaSize, EffectAreaType, MagicTradition, SpellComponent, SpellTrait } from "./types.ts"; +import { EffectAreaSize, EffectAreaType, MagicTradition, SpellTrait } from "./types.ts"; type SpellSource = BaseItemSourcePF2e<"spell", SpellSystemSource>; interface SpellSystemSource extends ItemSystemSource { traits: SpellTraits; level: { value: OneToTen }; - spellType: { - value: keyof ConfigPF2e["PF2E"]["spellTypes"]; - }; - category: { - value: keyof ConfigPF2e["PF2E"]["spellCategories"]; - }; - traditions: { value: MagicTradition[] }; - components: Record; - materials: { - value: string; - }; + requirements: string; target: { value: string; }; @@ -32,27 +22,17 @@ interface SpellSystemSource extends ItemSystemSource { }; duration: { value: string; + sustained: boolean; }; - damage: { - value: Record; - }; + damage: Record; heightening?: SpellHeighteningFixed | SpellHeighteningInterval; overlays?: Record; - save: { - basic: string; - value: SaveType | ""; - dc?: number; - str?: string; - }; - sustained: { - value: false; - }; + defense: SpellDefenseSource | null; cost: { value: string; }; - hasCounteractCheck: { - value: boolean; - }; + counteraction: boolean; + ritual: RitualData | null; location: { value: string | null; signature?: boolean; @@ -66,9 +46,9 @@ interface SpellSystemSource extends ItemSystemSource { }; } -interface SpellSystemData extends SpellSystemSource, Omit {} - -type SpellTraits = ItemTraits; +interface SpellTraits extends ItemTraits { + traditions: MagicTradition[]; +} interface SpellArea { value: EffectAreaSize; @@ -80,16 +60,17 @@ interface SpellArea { details?: string; } -interface SpellDamageType { - value: DamageType; - subtype?: DamageCategoryUnique; - categories: MaterialDamageEffect[]; +interface SpellDamageSource { + formula: string; + kinds?: ("damage" | "healing")[]; + applyMod?: boolean; + type: DamageType; + category: DamageCategoryUnique | null; + materials: MaterialDamageEffect[]; } -interface SpellDamage { - value: string; - applyMod?: boolean; - type: SpellDamageType; +interface SpellDefenseSource { + save: { statistic: SaveType; basic: boolean } | null; } interface SpellHeighteningInterval { @@ -122,17 +103,40 @@ interface SpellOverlayDamage { choices: DamageType[]; } +interface SpellSystemData extends Omit, Omit { + damage: Record; + defense: SpellDefenseData | null; +} + +interface SpellDamage extends Omit { + kinds: Set<"damage" | "healing">; +} + +interface SpellDefenseData extends SpellDefenseSource { + passive: { statistic: SpellPassiveDefense } | null; +} + +type SpellPassiveDefense = "ac" | `${SaveType}-dc`; type SpellOverlay = SpellOverlayOverride | SpellOverlayDamage; type SpellOverlayType = SpellOverlay["overlayType"]; +interface RitualData { + /** Details of the primary check for the ritual */ + primary: { check: string }; + /** Details of the secondary check(s) for the ritual and maximum number of casters */ + secondary: { checks: string; casters: number }; +} + export type { SpellArea, SpellDamage, + SpellDamageSource, SpellHeightenLayer, SpellHeighteningInterval, SpellOverlay, SpellOverlayOverride, SpellOverlayType, + SpellPassiveDefense, SpellSource, SpellSystemData, SpellSystemSource, diff --git a/src/module/item/spell/document.ts b/src/module/item/spell/document.ts index eeabe971ddc..83109204682 100644 --- a/src/module/item/spell/document.ts +++ b/src/module/item/spell/document.ts @@ -1,6 +1,7 @@ import type { ActorPF2e } from "@actor"; import { DamageDicePF2e, ModifierPF2e } from "@actor/modifiers.ts"; import { AttributeString } from "@actor/types.ts"; +import { SAVE_TYPES } from "@actor/values.ts"; import { ItemPF2e } from "@item"; import { ActionTrait } from "@item/ability/types.ts"; import { ItemSourcePF2e, ItemSummaryData } from "@item/base/data/index.ts"; @@ -43,8 +44,8 @@ import { htmlClosest, localizer, ordinalString, - setHasElement, traitSlugToObject, + tupleHasValue, } from "@util"; import * as R from "remeda"; import { @@ -55,9 +56,9 @@ import { SpellSystemData, SpellSystemSource, } from "./data.ts"; +import { createSpellRankLabel } from "./helpers.ts"; import { SpellOverlayCollection } from "./overlay.ts"; -import { EffectAreaSize, MagicSchool, MagicTradition, SpellComponent, SpellTrait } from "./types.ts"; -import { MAGIC_SCHOOLS } from "./values.ts"; +import { EffectAreaSize, MagicTradition, SpellTrait } from "./types.ts"; interface SpellConstructionContext extends DocumentConstructionContext { fromConsumable?: boolean; @@ -123,24 +124,13 @@ class SpellPF2e extends Ite /** Action traits added when Casting this Spell */ get castingTraits(): ActionTrait[] { - const { components } = this; - return ( - [ - getActionIcon(this.system.time.value, null) === null ? "exploration" : [], - components.verbal ? "concentrate" : [], - (["focus", "material", "somatic"] as const).some((c) => components[c]) ? "manipulate" : [], - ] as const - ).flat(); - } - - get school(): MagicSchool | null { - return this.system.traits.value.find((t): t is MagicSchool => setHasElement(MAGIC_SCHOOLS, t)) ?? null; + return R.compact([getActionIcon(this.system.time.value, null) === null ? "exploration" : null]); } get traditions(): Set { return this.spellcasting?.tradition ? new Set([this.spellcasting.tradition]) - : new Set(this.system.traditions.value); + : new Set(this.system.traits.traditions); } get spellcasting(): BaseSpellcastingEntry> | null { @@ -152,7 +142,7 @@ class SpellPF2e extends Ite } get isAttack(): boolean { - return this.traits.has("attack") || this.system.spellType.value === "attack"; + return this.traits.has("attack"); } get isCantrip(): boolean { @@ -160,11 +150,11 @@ class SpellPF2e extends Ite } get isFocusSpell(): boolean { - return this.system.category.value === "focus"; + return this.traits.has("focus"); } get isRitual(): boolean { - return this.system.category.value === "ritual"; + return !!this.system.ritual; } get attribute(): AttributeString { @@ -180,19 +170,6 @@ class SpellPF2e extends Ite return this.attribute; } - get components(): Record & { value: string } { - const components = this.system.components; - const results: string[] = []; - if (components.focus) results.push(game.i18n.localize("PF2E.SpellComponentShortF")); - if (components.material) results.push(game.i18n.localize("PF2E.SpellComponentShortM")); - if (components.somatic) results.push(game.i18n.localize("PF2E.SpellComponentShortS")); - if (components.verbal) results.push(game.i18n.localize("PF2E.SpellComponentShortV")); - return { - ...components, - value: results.join(""), - }; - } - /** Whether this spell has unlimited uses */ get unlimited(): boolean { // In the future handle at will and constant @@ -224,13 +201,7 @@ class SpellPF2e extends Ite /** Whether the "damage" roll of this spell deals damage or heals (or both, depending on the target) */ get damageKinds(): Set { - return new Set( - this.system.spellType.value === "heal" - ? this.system.save.value - ? ["damage", "healing"] - : ["healing"] - : ["damage"], - ); + return new Set(Object.values(this.system.damage).flatMap((d) => Array.from(d.kinds))); } override get uuid(): ItemUUID { @@ -270,7 +241,7 @@ class SpellPF2e extends Ite async getDamage(params: SpellDamageOptions = { skipDialog: true }): Promise { // Return early if the spell doesn't deal damage - if (!Object.keys(this.system.damage.value).length || !this.actor || !this.spellcasting?.statistic) { + if (!Object.keys(this.system.damage).length || !this.actor || !this.spellcasting?.statistic) { return null; } @@ -279,13 +250,13 @@ class SpellPF2e extends Ite // Loop over the user defined damage fields const base: BaseDamageData[] = []; - for (const [id, damage] of Object.entries(this.system.damage.value ?? {})) { - if (!DamageRoll.validate(damage.value)) { - console.error(`Failed to parse damage formula "${damage.value}"`); + for (const [id, damage] of Object.entries(this.system.damage ?? {})) { + if (!DamageRoll.validate(damage.formula)) { + console.error(`Failed to parse damage formula "${damage.formula}"`); return null; } - const terms = parseTermsFromSimpleFormula(damage.value, { rollData }); + const terms = parseTermsFromSimpleFormula(damage.formula, { rollData }); // Check for and apply interval spell scaling const heightening = this.system.heightening; @@ -306,9 +277,9 @@ class SpellPF2e extends Ite terms.push({ dice: null, modifier: this.actor.isElite ? value : -value }); } - const damageType = damage.type.value; - const category = damage.type.subtype || null; - const materials = damage.type.categories; + const damageType = damage.type; + const category = damage.category || null; + const materials = damage.materials; base.push({ terms: combinePartialTerms(terms), damageType, category, materials }); } @@ -355,7 +326,7 @@ class SpellPF2e extends Ite const { actor } = contextData.self; if (actor.system.abilities) { const attributes = actor.system.abilities; - const attributeModifiers = Object.entries(this.system.damage.value) + const attributeModifiers = Object.entries(this.system.damage) .filter(([, d]) => d.applyMod) .map( ([k, d]) => @@ -365,8 +336,8 @@ class SpellPF2e extends Ite // Not a restricted attribute modifier in the same way it is for checks or weapon damage type: "untyped", modifier: attributes[attribute].mod, - damageType: d.type.value, - damageCategory: d.type.subtype || null, + damageType: d.type, + damageCategory: d.category || null, adjustments: extractModifierAdjustments( actor.synthetics.modifierAdjustments, domains, @@ -564,7 +535,7 @@ class SpellPF2e extends Ite // Show all traditions as traits if there is no actor if (!this.isEmbedded) { - this.system.traits.value.push(...this.system.traditions.value); + this.system.traits.value.push(...this.system.traits.traditions); } // In case bad level data somehow made it in @@ -577,20 +548,39 @@ class SpellPF2e extends Ite this.system.area = null; } - if (this.isRitual) this.system.location.value = "rituals"; + if (this.isRitual) { + this.system.damage = {}; + this.system.defense = null; + this.system.traits.value = this.system.traits.value.filter( + (t) => !["attack", "cantrip", "focus"].includes(t), + ); + this.system.traits.traditions = []; + this.system.location.value = "rituals"; + } + + if (this.system.traits.value.includes("attack")) { + this.system.defense = mergeObject(this.system.defense ?? {}, { + passive: { statistic: "ac" as const }, + save: this.system.defense?.save ?? null, + }); + } // Ensure formulas are never empty string and default to 0 - for (const formula of Object.values(this.system.damage.value)) { - formula.value = formula.value?.trim() || "0"; + for (const damage of Object.values(this.system.damage)) { + damage.formula = damage.formula?.trim() || "0"; + + damage.kinds = new Set(damage.kinds); + if (damage.kinds.size === 0) damage.kinds.add("damage"); } + if (this.system.heightening?.type === "fixed") { for (const heighten of Object.values(this.system.heightening.levels)) { - for (const formula of Object.values(heighten.damage?.value ?? {})) { - formula.value = formula.value?.trim() || "0"; + for (const formula of Object.values(heighten.damage ?? {})) { + formula.formula = formula.formula?.trim() || "0"; } } } else if (this.system.heightening?.type === "interval") { - for (const key of Object.keys(this.system.heightening.damage)) { + for (const key of Object.keys(this.system.heightening.damage ?? {})) { this.system.heightening.damage[key] = this.system.heightening.damage[key]?.trim() || "0"; } } @@ -621,25 +611,24 @@ class SpellPF2e extends Ite options.add(`${prefix}:frequency:limited`); } - const damageValues = Object.values(this.system.damage.value); - for (const damage of damageValues) { + for (const damage of Object.values(this.system.damage)) { if (damage.type) { - options.add(`${prefix}:damage:${damage.type.value}`); - options.add(`${prefix}:damage:type:${damage.type.value}`); + options.add(`${prefix}:damage:${damage.type}`); + options.add(`${prefix}:damage:type:${damage.type}`); } - const category = DamageCategorization.fromDamageType(damage.type.value); + const category = DamageCategorization.fromDamageType(damage.type); if (category) { options.add(`${prefix}:damage:category:${category}`); } - if (damage.type.subtype === "persistent") { - options.add(`${prefix}:damage:persistent:${damage.type.value}`); + if (damage.category === "persistent") { + options.add(`${prefix}:damage:persistent:${damage.type}`); } } const isAreaEffect = !!this.system.area?.value; if (isAreaEffect) options.add("area-effect"); - if (damageValues.length > 0 && this.system.spellType.value !== "heal") { + if (Object.values(this.system.damage).some((d) => d.kinds.has("damage"))) { options.add("damaging-effect"); if (isAreaEffect) options.add("area-damage"); } @@ -689,7 +678,7 @@ class SpellPF2e extends Ite }; // The only data that can possibly exist in a casted spell is the dc, so we pull that data. - if (this.system.spellType.value === "save" || this.system.save.value !== "") { + if (this.system.defense) { const dc = entry.statistic.withRollOptions({ item: this }).dc; flags.context = { type: "spell-cast", @@ -739,7 +728,6 @@ class SpellPF2e extends Ite const rollData = htmlOptions.rollData ?? this.getRollData({ castLevel }); rollData.item ??= this; - const localize: Localization["localize"] = game.i18n.localize.bind(game.i18n); const systemData: SpellSystemData = this.system; const options = { ...htmlOptions, rollData }; @@ -763,13 +751,16 @@ class SpellPF2e extends Ite const statisticChatData = statistic?.getChatData({ item: this }); const spellDC = statisticChatData?.dc.value; - const isSave = systemData.spellType.value === "save" || !!systemData.save.value; const damage = await this.getDamage(); const hasDamage = !!damage; // needs new check // formula && formula !== "0"; // Spell save label - const saveType = systemData.save.value ? game.i18n.localize(CONFIG.PF2E.saves[systemData.save.value]) : null; - const saveKey = systemData.save.basic ? "PF2E.SaveDCLabelBasic" : "PF2E.SaveDCLabel"; + const saveType = + systemData.defense?.save && tupleHasValue(SAVE_TYPES, systemData.defense.save.statistic) + ? game.i18n.localize(CONFIG.PF2E.saves[systemData.defense.save.statistic]) + : null; + const isSave = !!saveType; + const saveKey = systemData.defense?.save?.basic ? "PF2E.SaveDCLabelBasic" : "PF2E.SaveDCLabel"; const saveLabel = ((): string | null => { if (!(spellDC && saveType)) return null; const localized = game.i18n.format(saveKey, { dc: spellDC, type: saveType }); @@ -789,24 +780,22 @@ class SpellPF2e extends Ite const { baseRank } = this; const heightened = castLevel - baseRank; - const rankLabel = (() => { - const type = this.isCantrip - ? localize("PF2E.TraitCantrip") - : localize(CONFIG.PF2E.spellCategories[this.system.category.value]); - return game.i18n.format("PF2E.ItemLevel", { type, level: castLevel }); - })(); + const rankLabel = createSpellRankLabel(this, castLevel); // Combine properties const area = this.area; const properties = R.compact([ heightened ? game.i18n.format("PF2E.SpellLevelBase", { level: ordinalString(baseRank) }) : null, heightened ? game.i18n.format("PF2E.SpellLevelHeightened", { heightened }) : null, - this.isRitual ? null : `${localize("PF2E.SpellComponentsLabel")}: ${this.components.value}`, - systemData.range.value ? `${localize("PF2E.SpellRangeLabel")}: ${systemData.range.value}` : null, - systemData.target.value ? `${localize("PF2E.SpellTargetLabel")}: ${systemData.target.value}` : null, + systemData.range.value ? `${game.i18n.localize("PF2E.SpellRangeLabel")}: ${systemData.range.value}` : null, + systemData.target.value + ? `${game.i18n.localize("PF2E.SpellTargetLabel")}: ${systemData.target.value}` + : null, area ? game.i18n.format("PF2E.SpellArea", { area: area.label }) : null, - systemData.time.value ? `${localize("PF2E.SpellTimeLabel")}: ${systemData.time.value}` : null, - systemData.duration.value ? `${localize("PF2E.SpellDurationLabel")}: ${systemData.duration.value}` : null, + systemData.time.value ? `${game.i18n.localize("PF2E.Item.Spell.Cast")}: ${systemData.time.value}` : null, + systemData.duration.value + ? `${game.i18n.localize("PF2E.SpellDurationLabel")}: ${systemData.duration.value}` + : null, ]); const spellTraits = this.traitChatData(CONFIG.PF2E.spellTraits); @@ -824,7 +813,7 @@ class SpellPF2e extends Ite check: this.isAttack && statisticChatData ? statisticChatData.check : undefined, save: { ...(statisticChatData?.dc ?? {}), - type: systemData.save.value, + type: saveType, label: saveLabel, }, hasDamage, @@ -980,8 +969,15 @@ class SpellPF2e extends Ite user: UserPF2e, ): Promise { this._source.system.location.value ||= null; - if (this._source.system.category.value === "ritual") { + + if (this._source.system.ritual) { + this._source.system.damage = {}; + this._source.system.defense = null; this._source.system.location.value = null; + this._source.system.traits.value = this._source.system.traits.value.filter( + (t) => !["attack", "cantrip", "focus"].includes(t), + ); + this._source.system.traits.traditions = []; } return super._preCreate(data, options, user); @@ -1003,6 +999,13 @@ class SpellPF2e extends Ite level.value = Math.clamped(Math.trunc(Number(level.value) || 1), 1, 10) as OneToTen; } + // Wipe defense data if `defense.save.statistic` is set to empty string + const save: { statistic?: string; basic?: boolean } = changed.system?.defense?.save ?? {}; + if (changed.system && save.statistic === "") { + delete changed.system?.defense; + changed.system.defense = null; + } + const uses = changed.system?.location?.uses; if (uses) { const currentUses = uses.value ?? this.system.location.uses?.value ?? 1; diff --git a/src/module/item/spell/helpers.ts b/src/module/item/spell/helpers.ts new file mode 100644 index 00000000000..8e3b69b86e0 --- /dev/null +++ b/src/module/item/spell/helpers.ts @@ -0,0 +1,15 @@ +import type { SpellPF2e } from "./index.ts"; + +function createSpellRankLabel(spell: SpellPF2e, castRank?: number): string { + const typeLabel = spell.isCantrip + ? game.i18n.localize("PF2E.TraitCantrip") + : spell.isFocusSpell + ? game.i18n.localize("PF2E.TraitFocus") + : spell.isRitual + ? game.i18n.localize("PF2E.Item.Spell.Ritual.Label") + : game.i18n.localize("TYPES.Item.spell"); + + return castRank ? game.i18n.format("ItemLevel", { type: typeLabel, level: castRank }) : typeLabel; +} + +export { createSpellRankLabel }; diff --git a/src/module/item/spell/sheet.ts b/src/module/item/spell/sheet.ts index 5d5a8580c59..4ef6c2a073a 100644 --- a/src/module/item/spell/sheet.ts +++ b/src/module/item/spell/sheet.ts @@ -1,5 +1,4 @@ -import { ActorPF2e } from "@actor"; -import { SpellPF2e, SpellSystemSource } from "@item/spell/index.ts"; +import type { ActorPF2e } from "@actor"; import { OneToTen } from "@module/data.ts"; import { TraitTagifyEntry, createTagifyTraits } from "@module/sheet/helpers.ts"; import { DamageCategoryUnique } from "@system/damage/types.ts"; @@ -17,13 +16,19 @@ import { } from "@util"; import * as R from "remeda"; import { ItemSheetDataPF2e, ItemSheetPF2e } from "../base/sheet/base.ts"; -import { SpellDamage, SpellHeighteningInterval, SpellSystemData } from "./data.ts"; +import { createSpellRankLabel } from "./helpers.ts"; +import type { + SpellDamageSource, + SpellHeighteningInterval, + SpellPF2e, + SpellSystemData, + SpellSystemSource, +} from "./index.ts"; /** Set of properties that are legal for the purposes of spell overrides */ const spellOverridable: Partial> = { traits: "PF2E.Traits", - time: "PF2E.SpellTimeLabel", - components: "PF2E.SpellComponentsLabel", + time: "PF2E.Item.Spell.Cast", target: "PF2E.SpellTargetLabel", area: "PF2E.AreaLabel", range: "PF2E.SpellRangeLabel", @@ -44,15 +49,6 @@ export class SpellSheetPF2e extends ItemSheetPF2e { const sheetData = await super.getData(options); const { isCantrip, isFocusSpell, isRitual } = this.item; - // Create a level label to show in the summary. - // This one is a longer version than the chat card - const itemType = - isCantrip && isFocusSpell - ? game.i18n.localize("PF2E.SpellCategoryFocusCantrip") - : this.item.isCantrip - ? game.i18n.localize("PF2E.TraitCantrip") - : game.i18n.localize(CONFIG.PF2E.spellCategories[this.item.system.category.value]); - const damageTypes = Object.fromEntries( Object.entries(CONFIG.PF2E.damageTypes) .map(([slug, localizeKey]): [string, string] => [slug, game.i18n.localize(localizeKey)]) @@ -66,30 +62,42 @@ export class SpellSheetPF2e extends ItemSheetPF2e { sort: variant.sort, actions: getActionGlyph(variant.system.time.value), })) - .sort((variantA, variantB) => variantA.sort - variantB.sort); + .sort((a, b) => a.sort - b.sort); + + const passiveDefense = ((): string | null => { + const statistic = this.item.system.defense?.passive?.statistic; + switch (statistic) { + case "ac": + return "PF2E.Check.DC.Specific.armor"; + case "fortitude-dc": + return "PF2E.Check.DC.Specific.fortitude"; + case "reflex-dc": + return "PF2E.Check.DC.Specific.reflex"; + case "will-dc": + return "PF2E.Check.DC.Specific.will"; + default: + return null; + } + })(); return { ...sheetData, hasSidebar: true, - itemType, + itemType: createSpellRankLabel(this.item), isCantrip, isFocusSpell, isRitual, + passiveDefense, variants, isVariant: this.item.isVariant, - spellCategories: CONFIG.PF2E.spellCategories, - spellTypes: CONFIG.PF2E.spellTypes, - saves: CONFIG.PF2E.saves, - magicSchools: CONFIG.PF2E.magicSchools, damageTypes, damageSubtypes: R.pick(CONFIG.PF2E.damageCategories, [...DAMAGE_CATEGORIES_UNIQUE]), damageCategories: CONFIG.PF2E.damageCategories, - spellComponents: this.#formatSpellComponents(sheetData.data), areaSizes: CONFIG.PF2E.areaSizes, areaTypes: CONFIG.PF2E.areaTypes, heightenIntervals: [1, 2, 3, 4], heightenOverlays: this.#prepareHeighteningLevels(), - canHeighten: this.getAvailableHeightenLevels().length > 0, + canHeighten: this.isEditable && this.getAvailableHeightenLevels().length > 0, }; } @@ -114,69 +122,57 @@ export class SpellSheetPF2e extends ItemSheetPF2e { super.activateListeners($html); const html = $html[0]!; - tagify(html.querySelector('input[name="system.traditions.value"]'), { whitelist: CONFIG.PF2E.magicTraditions }); + tagify(html.querySelector('input[name="system.traits.traditions"]'), { + whitelist: CONFIG.PF2E.magicTraditions, + }); for (const tags of htmlQueryAll(html, "input.spell-traits")) { - tagify(tags, { whitelist: CONFIG.PF2E.spellTraits }); + tagify(tags, { + whitelist: this.item.isRitual + ? R.omit(CONFIG.PF2E.spellTraits, ["attack", "cantrip", "focus"]) + : CONFIG.PF2E.spellTraits, + }); } - $html.find(".toggle-trait").on("change", (evt) => { - const target = evt.target as HTMLInputElement; - const trait = target.dataset.trait ?? ""; - if (!objectHasKey(CONFIG.PF2E.spellTraits, trait)) { - console.warn("Toggled trait is invalid"); - return; - } - - if (target.checked && !this.item.traits.has(trait)) { - const newTraits = this.item.system.traits.value.concat([trait]); - this.item.update({ "system.traits.value": newTraits }); - } else if (!target.checked && this.item.traits.has(trait)) { - const newTraits = this.item.system.traits.value.filter((t) => t !== trait); - this.item.update({ "system.traits.value": newTraits }); - } - }); - - $html.find("[data-action=damage-create]").on("click", (event) => { - event.preventDefault(); - const overlayData = this.#getOverlayFromElement(event.target); + const addDamageAnchor = htmlQuery(html, "a[data-action=add-damage-partial]"); + addDamageAnchor?.addEventListener("click", () => { + const overlayData = this.#getOverlayFromElement(addDamageAnchor); const baseKey = overlayData?.base ?? "system"; - const emptyDamage: SpellDamage = { value: "", type: { value: "bludgeoning", categories: [] } }; + const emptyDamage: SpellDamageSource = { + formula: "", + kinds: ["damage"], + type: "bludgeoning", + category: null, + materials: [], + }; this.item.update({ [`${baseKey}.damage.value.${randomID()}`]: emptyDamage }); }); - $html.find("[data-action=damage-delete]").on("click", (event) => { - event.preventDefault(); - const overlayData = this.#getOverlayFromElement(event.target); + const removeDamageAnchor = htmlQuery(html, "a[data-action=delete-damage-partial]"); + removeDamageAnchor?.addEventListener("click", () => { + const overlayData = this.#getOverlayFromElement(removeDamageAnchor); const baseKey = overlayData?.base ?? "system"; - const id = $(event.target).closest("[data-action=damage-delete]").attr("data-id"); - if (id) { - const values = { [`${baseKey}.damage.value.-=${id}`]: null }; + const key = htmlClosest(removeDamageAnchor, "[data-action=damage-delete]")?.dataset.id; + if (key) { + const values = { [`${baseKey}.damage.value.-=${key}`]: null }; if (!overlayData) { - values[`${baseKey}.heightening.damage.-=${id}`] = null; + values[`${baseKey}.heightening.damage.-=${key}`] = null; } this.item.update(values); } }); - for (const button of htmlQueryAll(html, "[data-action=heightening-interval-create]")) { - button.addEventListener("click", (event) => { - event.preventDefault(); - const baseKey = this.#getOverlayFromElement(event.target)?.base ?? "system"; - const data: SpellHeighteningInterval = { - type: "interval", - interval: 1, - damage: R.mapToObj(Object.keys(this.item.system.damage.value), (key) => [key, "0"]), - }; - this.item.update({ [`${baseKey}.heightening`]: data }); - }); - } - - // Event used to delete all heightening, not just a particular one - $html.find("[data-action=heightening-delete]").on("click", () => { - this.item.update({ "system.-=heightening": null }); + htmlQuery(html, "button[data-action=add-interval-heightening]")?.addEventListener("click", (event) => { + event.preventDefault(); + const baseKey = this.#getOverlayFromElement(event.target)?.base ?? "system"; + const data: SpellHeighteningInterval = { + type: "interval", + interval: 1, + damage: R.mapToObj(Object.keys(this.item.system.damage), (key) => [key, "0"]), + }; + this.item.update({ [`${baseKey}.heightening`]: data }); }); - $html.find("[data-action=heightening-fixed-create]").on("click", () => { + htmlQuery(html, "button[data-action=add-fixed-heightening]")?.addEventListener("click", () => { const highestLevel = this.item.getHeightenLayers().at(-1)?.level; const available = this.getAvailableHeightenLevels(); const level = highestLevel && highestLevel < 10 ? highestLevel + 1 : available.at(0); @@ -185,12 +181,17 @@ export class SpellSheetPF2e extends ItemSheetPF2e { this.item.update({ "system.heightening": { type: "fixed", levels: { [level]: {} } } }); }); + // Event used to delete all heightening, not just a particular one + htmlQuery(html, "a[data-action=delete-heightening]")?.addEventListener("click", () => { + this.item.update({ "system.-=heightening": null }); + }); + // Add event handlers for heighten type overlays for (const overlayEditor of htmlQueryAll(html, "[data-overlay-type=heighten]")) { const overlay = this.#getOverlayFromElement(overlayEditor); if (!overlay) continue; - htmlQuery(overlayEditor, "[data-action=overlay-delete]")?.addEventListener("click", () => { + htmlQuery(overlayEditor, "a[data-action=delete-overlay]")?.addEventListener("click", () => { // If this is the last heighten overlay, delete all of it if (overlay.type === "heighten") { const layers = this.item.getHeightenLayers(); @@ -206,7 +207,7 @@ export class SpellSheetPF2e extends ItemSheetPF2e { }); // Handle adding properties to overlays - for (const addProperty of htmlQueryAll(overlayEditor, "[data-action=overlay-add-property]")) { + for (const addProperty of htmlQueryAll(overlayEditor, "button[data-action=add-overlay-property]")) { const property = addProperty.dataset.property; if (!overlay.system || !property || property in overlay.system) continue; addProperty.addEventListener("click", () => { @@ -222,7 +223,7 @@ export class SpellSheetPF2e extends ItemSheetPF2e { } // Handle deleting properties from overlays - for (const removeProperty of htmlQueryAll(overlayEditor, "[data-action=overlay-remove-property]")) { + for (const removeProperty of htmlQueryAll(overlayEditor, "a[data-action=delete-overlay-property]")) { const property = removeProperty.dataset.property; if (!property) continue; removeProperty.addEventListener("click", () => { @@ -234,7 +235,10 @@ export class SpellSheetPF2e extends ItemSheetPF2e { }); } - const levelSelect = htmlQuery(overlayEditor, "[data-action=change-heighten-level]"); + const levelSelect = htmlQuery( + overlayEditor, + "select[data-action=change-heighten-level]", + ); levelSelect?.addEventListener("change", () => { const newLevel = Number(levelSelect.value); const existingData = this.item.getHeightenLayers().find((layer) => layer.level === overlay.level); @@ -245,47 +249,63 @@ export class SpellSheetPF2e extends ItemSheetPF2e { }); } - $html.find("[data-action=variant-create]").on("click", () => { + htmlQuery(html, "a[data-action=variant-create]")?.addEventListener("click", () => { this.item.overlays.create("override"); }); - $html.find("[data-action=variant-edit]").on("click", (event) => { - const id = $(event.target).closest("[data-action=variant-edit]").attr("data-id"); - if (id) { - this.item.loadVariant({ overlayIds: [id] })?.sheet.render(true); - } - }); - - $html.find("[data-action=variant-delete]").on("click", (event) => { - const id = $(event.target).closest("[data-action=variant-delete]").attr("data-id"); - if (id) { - const variant = this.item.loadVariant({ overlayIds: [id] }); - if (!variant) { - throw ErrorPF2e( - `Spell ${this.item.name} (${this.item.uuid}) does not have a variant with id: ${id}`, - ); + for (const anchor of htmlQueryAll(html, "a[data-action=edit-variant]")) { + anchor.addEventListener("click", () => { + const overlayId = anchor.dataset.id; + if (overlayId) { + this.item.loadVariant({ overlayIds: [overlayId] })?.sheet.render(true); } - new Dialog({ - title: game.i18n.localize("PF2E.Item.Spell.Variants.DeleteDialogTitle"), - content: `

${game.i18n.format("PF2E.Item.Spell.Variants.DeleteDialogText", { - variantName: variant.name, - })}

`, - buttons: { - delete: { - icon: fontAwesomeIcon("fa-trash").outerHTML, - label: game.i18n.localize("PF2E.DeleteShortLabel"), - callback: () => { - this.item.overlays.deleteOverlay(id); + }); + } + + for (const anchor of htmlQueryAll(html, "a[data-action=delete-variant]")) { + anchor.addEventListener("click", () => { + const id = anchor.dataset.id; + if (id) { + const variant = this.item.loadVariant({ overlayIds: [id] }); + if (!variant) { + throw ErrorPF2e( + `Spell ${this.item.name} (${this.item.uuid}) does not have a variant with id: ${id}`, + ); + } + new Dialog({ + title: game.i18n.localize("PF2E.Item.Spell.Variants.DeleteDialogTitle"), + content: `

${game.i18n.format("PF2E.Item.Spell.Variants.DeleteDialogText", { + variantName: variant.name, + })}

`, + buttons: { + delete: { + icon: fontAwesomeIcon("fa-trash").outerHTML, + label: game.i18n.localize("PF2E.DeleteShortLabel"), + callback: () => { + this.item.overlays.deleteOverlay(id); + }, + }, + cancel: { + icon: fontAwesomeIcon("fa-times").outerHTML, + label: game.i18n.localize("Cancel"), }, }, - cancel: { - icon: fontAwesomeIcon("fa-times").outerHTML, - label: game.i18n.localize("Cancel"), - }, - }, - default: "cancel", - }).render(true); - } + default: "cancel", + }).render(true); + } + }); + } + + const ritualCheckbox = htmlQuery(html, "input[data-action=toggle-ritual-data]"); + ritualCheckbox?.addEventListener("click", async (event) => { + event.preventDefault(); + ritualCheckbox.readOnly = true; + + await this.item.update({ + "system.ritual": this.item.system.ritual + ? null + : { primary: { check: "" }, secondary: { checks: "", casters: 0 } }, + }); }); } @@ -414,17 +434,6 @@ export class SpellSheetPF2e extends ItemSheetPF2e { throw ErrorPF2e(`Failed to initialize property ${property} for overlay`); } - #formatSpellComponents(data: SpellSystemData): string[] { - if (!data.components) return []; - const comps: string[] = []; - if (data.components.focus) comps.push(game.i18n.localize(CONFIG.PF2E.spellComponents.F)); - if (data.components.material) comps.push(game.i18n.localize(CONFIG.PF2E.spellComponents.M)); - if (data.components.somatic) comps.push(game.i18n.localize(CONFIG.PF2E.spellComponents.S)); - if (data.components.verbal) comps.push(game.i18n.localize(CONFIG.PF2E.spellComponents.V)); - if (data.materials.value) comps.push(data.materials.value); - return comps; - } - #prepareHeighteningLevels(): SpellSheetHeightenOverlayData[] { const spell = this.item; const layers = spell.getHeightenLayers(); @@ -458,6 +467,7 @@ interface SpellSheetData extends ItemSheetDataPF2e { isCantrip: boolean; isFocusSpell: boolean; isRitual: boolean; + passiveDefense: string | null; isVariant: boolean; variants: { name: string; @@ -465,16 +475,11 @@ interface SpellSheetData extends ItemSheetDataPF2e { sort: number; actions: string; }[]; - magicSchools: ConfigPF2e["PF2E"]["magicSchools"]; - spellCategories: ConfigPF2e["PF2E"]["spellCategories"]; - spellTypes: ConfigPF2e["PF2E"]["spellTypes"]; - saves: ConfigPF2e["PF2E"]["saves"]; - damageCategories: ConfigPF2e["PF2E"]["damageCategories"]; + damageCategories: typeof CONFIG.PF2E.damageCategories; damageTypes: Record; - damageSubtypes: Pick; - spellComponents: string[]; - areaSizes: ConfigPF2e["PF2E"]["areaSizes"]; - areaTypes: ConfigPF2e["PF2E"]["areaTypes"]; + damageSubtypes: Pick; + areaSizes: typeof CONFIG.PF2E.areaSizes; + areaTypes: typeof CONFIG.PF2E.areaTypes; heightenIntervals: number[]; heightenOverlays: SpellSheetHeightenOverlayData[]; canHeighten: boolean; diff --git a/src/module/item/spell/types.ts b/src/module/item/spell/types.ts index 985fb9d9775..854c8c1a6fa 100644 --- a/src/module/item/spell/types.ts +++ b/src/module/item/spell/types.ts @@ -1,11 +1,9 @@ -import type { MAGIC_SCHOOLS, MAGIC_TRADITIONS, SPELL_COMPONENTS } from "./values.ts"; +import type { MAGIC_TRADITIONS } from "./values.ts"; -type MagicSchool = SetElement; type MagicTradition = SetElement; -type SpellComponent = (typeof SPELL_COMPONENTS)[number]; -type SpellTrait = keyof ConfigPF2e["PF2E"]["spellTraits"] | MagicSchool | MagicTradition; +type SpellTrait = keyof typeof CONFIG.PF2E.spellTraits | MagicTradition; -type EffectAreaSize = keyof ConfigPF2e["PF2E"]["areaSizes"]; -type EffectAreaType = keyof ConfigPF2e["PF2E"]["areaTypes"]; +type EffectAreaSize = keyof typeof CONFIG.PF2E.areaSizes; +type EffectAreaType = keyof typeof CONFIG.PF2E.areaTypes; -export type { EffectAreaSize, EffectAreaType, MagicSchool, MagicTradition, SpellComponent, SpellTrait }; +export type { EffectAreaSize, EffectAreaType, MagicTradition, SpellTrait }; diff --git a/src/module/item/spell/values.ts b/src/module/item/spell/values.ts index 6f3fa22ee00..fb48d2622d6 100644 --- a/src/module/item/spell/values.ts +++ b/src/module/item/spell/values.ts @@ -11,6 +11,4 @@ const MAGIC_SCHOOLS = new Set([ const MAGIC_TRADITIONS = new Set(["arcane", "divine", "occult", "primal"] as const); -const SPELL_COMPONENTS = ["focus", "material", "somatic", "verbal"] as const; - -export { MAGIC_SCHOOLS, MAGIC_TRADITIONS, SPELL_COMPONENTS }; +export { MAGIC_SCHOOLS, MAGIC_TRADITIONS }; diff --git a/src/module/item/weapon/document.ts b/src/module/item/weapon/document.ts index ec6f98d1c7a..c0da9c9f967 100644 --- a/src/module/item/weapon/document.ts +++ b/src/module/item/weapon/document.ts @@ -368,12 +368,8 @@ class WeaponPF2e extends Ph const baseTraits = this.system.traits.value; const { runes } = this.system; const hasRunes = runes.potency > 0 || runes.striking > 0 || runes.property.length > 0; - const magicTraits: ("evocation" | "magical")[] = baseTraits.some((t) => setHasElement(MAGIC_TRADITIONS, t)) - ? ["evocation"] - : hasRunes - ? ["evocation", "magical"] - : []; - this.system.traits.value = R.uniq([baseTraits, magicTraits].flat()).sort(); + const magicTrait = hasRunes && !baseTraits.some((t) => setHasElement(MAGIC_TRADITIONS, t)) ? "magical" : null; + this.system.traits.value = R.uniq(R.compact([...baseTraits, magicTrait]).sort()); this.flags.pf2e.attackItemBonus = this.system.runes.potency || this.system.bonus.value || 0; } diff --git a/src/module/migration/migrations/605-catch-up-to-template-json.ts b/src/module/migration/migrations/605-catch-up-to-template-json.ts index b3a5fa1be76..acf6cf1b901 100644 --- a/src/module/migration/migrations/605-catch-up-to-template-json.ts +++ b/src/module/migration/migrations/605-catch-up-to-template-json.ts @@ -13,57 +13,57 @@ export class Migration605CatchUpToTemplateJSON extends MigrationBase { } } - override async updateActor(actorData: ActorSourcePF2e): Promise { - this.addEffects(actorData); + override async updateActor(source: ActorSourcePF2e): Promise { + this.addEffects(source); - if (actorData.type === "character" || actorData.type === "npc") { + if (source.type === "character" || source.type === "npc") { // Numeric HP max - if ("max" in actorData.system.attributes.hp && typeof actorData.system.attributes.hp.max === "string") { - const newMax = parseInt(actorData.system.attributes.hp.max as string, 10); + if ("max" in source.system.attributes.hp && typeof source.system.attributes.hp.max === "string") { + const newMax = parseInt(source.system.attributes.hp.max as string, 10); if (Number.isInteger(newMax)) { - actorData.system.attributes.hp.max = newMax; + source.system.attributes.hp.max = newMax; } } // Numeric HP value - if (typeof actorData.system.attributes.hp.value === "string") { - const newValue = parseInt(actorData.system.attributes.hp.value as string, 10); + if (typeof source.system.attributes.hp.value === "string") { + const newValue = parseInt(source.system.attributes.hp.value as string, 10); if (Number.isInteger(newValue)) { - actorData.system.attributes.hp.value = newValue; + source.system.attributes.hp.value = newValue; } } // Numeric level - if (typeof actorData.system.details.level.value === "string") { - const newLevel = parseInt(actorData.system.details.level.value as string, 10); + if (typeof source.system.details.level.value === "string") { + const newLevel = parseInt(source.system.details.level.value as string, 10); if (Number.isInteger(newLevel)) { - actorData.system.details.level.value = newLevel; + source.system.details.level.value = newLevel; } } // Remove unused/deprecated fields - if ("tempmax" in actorData.system.attributes.hp) { - delete (actorData.system.attributes.hp as { tempmax?: unknown }).tempmax; + if ("tempmax" in source.system.attributes.hp) { + delete (source.system.attributes.hp as { tempmax?: unknown }).tempmax; } - if ("special" in actorData.system.attributes.speed) { - delete (actorData.system.attributes.speed as { special?: unknown }).special; + if ("special" in source.system.attributes.speed) { + delete (source.system.attributes.speed as { special?: unknown }).special; } } } - override async updateItem(itemData: ItemSourcePF2e, actorData: ActorSourcePF2e): Promise { - this.addEffects(itemData); + override async updateItem(source: MaybeWithCounteractCheckObject, actorSource: ActorSourcePF2e): Promise { + this.addEffects(source); // Add slugs to owned items - if (!(itemData.system.slug as string | null) && actorData) { - itemData.system.slug = null; + if (!(source.system.slug as string | null) && actorSource) { + source.system.slug = null; } // Add rule elements - if (!Array.isArray(itemData.system.rules)) { - itemData.system.rules = []; + if (!Array.isArray(source.system.rules)) { + source.system.rules = []; } // Add custom trait field - const traits: TraitsWithRarityObject | undefined = itemData.system.traits; + const traits: TraitsWithRarityObject | undefined = source.system.traits; if (traits && !traits.custom) { traits.custom = ""; } @@ -73,29 +73,33 @@ export class Migration605CatchUpToTemplateJSON extends MigrationBase { } // Add item-identification property - if (isPhysicalData(itemData) && !itemData.system.identification) { - const withoutIdentifyData: { identification: { status: string } } = itemData.system; + if (isPhysicalData(source) && !source.system.identification) { + const withoutIdentifyData: { identification: { status: string } } = source.system; withoutIdentifyData.identification.status = "identified"; } // Add hasCounteractCheck property - if (itemData.type === "spell" && !itemData.system.hasCounteractCheck) { - itemData.system.hasCounteractCheck = { value: false }; + if (source.type === "spell" && !source.system.hasCounteractCheck) { + source.system.hasCounteractCheck = { value: false }; } // Remove unused fields - if (itemData.type === "lore" && "featType" in itemData.system) { - delete itemData.system.featType; + if (source.type === "lore" && "featType" in source.system) { + delete source.system.featType; } - if (itemData.type === "action" && "skill_requirements" in itemData.system) { - delete itemData.system.skill_requirements; + if (source.type === "action" && "skill_requirements" in source.system) { + delete source.system.skill_requirements; } - if (itemData.type === "action" && "skill_requirement" in itemData.system) { - itemData.system.skill_requirement; + if (source.type === "action" && "skill_requirement" in source.system) { + source.system.skill_requirement; } } } +type MaybeWithCounteractCheckObject = ItemSourcePF2e & { + system: { hasCounteractCheck?: object }; +}; + interface TraitsWithRarityObject { value?: string[]; rarity?: string | { value: string }; diff --git a/src/module/migration/migrations/621-remove-config-spell-schools.ts b/src/module/migration/migrations/621-remove-config-spell-schools.ts index 79f709c62b8..e06d62d03f8 100644 --- a/src/module/migration/migrations/621-remove-config-spell-schools.ts +++ b/src/module/migration/migrations/621-remove-config-spell-schools.ts @@ -1,8 +1,8 @@ import { ItemSourcePF2e } from "@item/base/data/index.ts"; +import { SpellSystemSource } from "@item/spell/index.ts"; import { MAGIC_SCHOOLS } from "@item/spell/values.ts"; import { objectHasKey, setHasElement } from "@util"; import { MigrationBase } from "../base.ts"; -import { MagicSchool, SpellSystemSource } from "@item/spell/index.ts"; /** Remove duplicate magic schools localization map */ export class Migration621RemoveConfigSpellSchools extends MigrationBase { @@ -29,9 +29,9 @@ export class Migration621RemoveConfigSpellSchools extends MigrationBase { } } - override async updateItem(itemData: ItemSourcePF2e): Promise { - if (itemData.type === "spell") { - const system: SpellSystemSourceWithSchool = itemData.system; + override async updateItem(source: ItemSourcePF2e): Promise { + if (source.type === "spell") { + const system: SpellSystemSourceWithSchool = source.system; const school: { value: string } = system.school ?? { value: "evocation" }; school.value = this.reassignSchool(school.value); } @@ -39,5 +39,5 @@ export class Migration621RemoveConfigSpellSchools extends MigrationBase { } interface SpellSystemSourceWithSchool extends SpellSystemSource { - school?: { value: MagicSchool }; + school?: { value: string }; } diff --git a/src/module/migration/migrations/626-update-spell-category.ts b/src/module/migration/migrations/626-update-spell-category.ts index 42b9022cf38..fd941cb54be 100644 --- a/src/module/migration/migrations/626-update-spell-category.ts +++ b/src/module/migration/migrations/626-update-spell-category.ts @@ -1,5 +1,6 @@ import { ItemSourcePF2e } from "@item/base/data/index.ts"; import { SpellSystemSource } from "@item/spell/data.ts"; +import { isObject } from "@util"; import { MigrationBase } from "../base.ts"; /** @@ -12,15 +13,13 @@ export class Migration626UpdateSpellCategory extends MigrationBase { override async updateItem(source: ItemSourcePF2e): Promise { if (source.type !== "spell") return; - interface MaybeCategorie extends Omit, "traditions"> { - traditions: { value: string[] }; - spellCategorie?: { value: "spell" | "focus" | "ritual" | "" }; - spellCategory?: { value: "spell" | "focus" | "ritual" | "" }; - "-=spellCategorie"?: unknown; - "-=spellCategory"?: unknown; - } const systemData: MaybeCategorie = source.system; const traditions = systemData.traditions; + if (!isObject(traditions) || !Array.isArray(traditions.value)) { + systemData.traditions = { value: [] }; + return; + } + const isFocus = traditions.value.includes("focus"); const isRitual = traditions.value.includes("ritual"); @@ -32,7 +31,7 @@ export class Migration626UpdateSpellCategory extends MigrationBase { if (systemData.spellCategorie || systemData.spellCategory) { const currentCategory = systemData.spellCategorie?.value ?? systemData.spellCategory?.value ?? ""; - source.system.category = { + systemData.category = { value: isFocus ? "focus" : isRitual ? "ritual" : currentCategory === "" ? "spell" : currentCategory, }; @@ -45,9 +44,22 @@ export class Migration626UpdateSpellCategory extends MigrationBase { } } - if (["focus", "ritual"].includes(source.system.spellType.value)) { + if ( + "spellType" in source.system && + isObject<{ value: string }>(source.system.spellType) && + ["focus", "ritual"].includes(source.system.spellType.value ?? "") + ) { source.system.spellType.value = "utility"; } traditions.value = traditions.value.filter((tradition) => !["focus", "ritual"].includes(tradition)); } } + +interface MaybeCategorie extends Omit, "traditions"> { + traditions?: { value: string[] }; + spellCategorie?: { value: "spell" | "focus" | "ritual" | "" }; + spellCategory?: { value: "spell" | "focus" | "ritual" | "" }; + category?: object; + "-=spellCategorie"?: unknown; + "-=spellCategory"?: unknown; +} diff --git a/src/module/migration/migrations/627-lowercase-spell-saves.ts b/src/module/migration/migrations/627-lowercase-spell-saves.ts index 266e06cd3bb..4ea757e7dd4 100644 --- a/src/module/migration/migrations/627-lowercase-spell-saves.ts +++ b/src/module/migration/migrations/627-lowercase-spell-saves.ts @@ -1,17 +1,25 @@ import { ItemSourcePF2e } from "@item/base/data/index.ts"; -import { tupleHasValue } from "@util"; +import { SpellSystemSource } from "@item/spell/data.ts"; +import { isObject, tupleHasValue } from "@util"; import { MigrationBase } from "../base.ts"; export class Migration627LowerCaseSpellSaves extends MigrationBase { static override version = 0.627; - override async updateItem(itemData: ItemSourcePF2e): Promise { - if (itemData.type !== "spell") return; - const saveType = itemData.system.save.value?.toLowerCase() ?? ""; + override async updateItem(source: ItemSourcePF2e): Promise { + if (source.type !== "spell") return; + const system: SpellSystemSourcewithSave = source.system; + const saveType = system.save?.value?.toLowerCase() ?? ""; + if (!isObject(system.save)) return; + if (tupleHasValue(["fortitude", "reflex", "will"] as const, saveType)) { - itemData.system.save.value = saveType; + system.save.value = saveType; } else { - itemData.system.save.value = ""; + system.save.value = ""; } } } + +interface SpellSystemSourcewithSave extends SpellSystemSource { + save?: { value?: string }; +} diff --git a/src/module/migration/migrations/638-spell-components.ts b/src/module/migration/migrations/638-spell-components.ts index b4803eecd6e..354927a16a2 100644 --- a/src/module/migration/migrations/638-spell-components.ts +++ b/src/module/migration/migrations/638-spell-components.ts @@ -1,18 +1,26 @@ -import { ItemSourcePF2e, SpellSource } from "@item/base/data/index.ts"; +import { ItemSourcePF2e } from "@item/base/data/index.ts"; +import { isObject } from "@util"; import { MigrationBase } from "../base.ts"; const validComponents = ["material", "somatic", "verbal"] as const; -type ComponentsOld = { value?: string; "-=value"?: null } & Partial; +type ComponentsOld = { [K in (typeof validComponents)[number] | "value" | "-=value"]?: string | boolean | null }; /** Convert spell components from a string value to VSM */ export class Migration638SpellComponents extends MigrationBase { static override version = 0.638; - override async updateItem(itemData: ItemSourcePF2e): Promise { - if (itemData.type !== "spell") return; + override async updateItem(source: ItemSourcePF2e): Promise { + if (source.type !== "spell") return; - const components: ComponentsOld = itemData.system.components; - const oldComponents = new Set(components.value?.split(",").map((v) => v.trim().toLowerCase())); + const components: ComponentsOld = + "components" in source.system && isObject(source.system.components) + ? source.system.components + : { value: "" }; + const oldComponents = new Set( + String(components.value) + .split(",") + .map((v) => v.trim().toLowerCase()), + ); for (const component of validComponents) { components[component] = components[component] || oldComponents.has(component); } diff --git a/src/module/migration/migrations/659-multiple-damage-rows.ts b/src/module/migration/migrations/659-multiple-damage-rows.ts index e6a357e06e8..19a12cbef9e 100644 --- a/src/module/migration/migrations/659-multiple-damage-rows.ts +++ b/src/module/migration/migrations/659-multiple-damage-rows.ts @@ -1,5 +1,5 @@ import { ItemSourcePF2e } from "@item/base/data/index.ts"; -import { SpellDamage, SpellSystemSource } from "@item/spell/data.ts"; +import { SpellSystemSource } from "@item/spell/data.ts"; import { DamageType } from "@system/damage/index.ts"; import { tupleHasValue } from "@util"; import { MigrationBase } from "../base.ts"; @@ -79,11 +79,7 @@ export class Migration659MultipleDamageRows extends MigrationBase { } interface SpellSystemDataOld extends Omit { - damage: { - value: string | Record; - applyMod?: boolean; - "-=applyMod"?: null; - }; + damage: Record; damageType?: { value?: DamageType; }; diff --git a/src/module/migration/migrations/663-fix-spell-damage.ts b/src/module/migration/migrations/663-fix-spell-damage.ts index 179e38a54fb..427f53a338f 100644 --- a/src/module/migration/migrations/663-fix-spell-damage.ts +++ b/src/module/migration/migrations/663-fix-spell-damage.ts @@ -63,7 +63,8 @@ export class Migration663FixSpellDamage extends MigrationBase { } } -interface SpellScalingOld extends SpellSystemSource { +interface SpellScalingOld extends Omit { + damage: Record; scaling?: { interval: number; damage: Record; diff --git a/src/module/migration/migrations/674-stable-homebrew-tag-ids.ts b/src/module/migration/migrations/674-stable-homebrew-tag-ids.ts index edffd3ffaab..a4ab802b33f 100644 --- a/src/module/migration/migrations/674-stable-homebrew-tag-ids.ts +++ b/src/module/migration/migrations/674-stable-homebrew-tag-ids.ts @@ -1,22 +1,22 @@ import { ActorSourcePF2e } from "@actor/data/index.ts"; import { ItemSourcePF2e } from "@item/base/data/index.ts"; -import { HomebrewTag, HOMEBREW_TRAIT_KEYS } from "@system/settings/homebrew/index.ts"; +import { HOMEBREW_TRAIT_KEYS, HomebrewTag } from "@system/settings/homebrew/index.ts"; import { sluggify } from "@util"; import { MigrationBase } from "../base.ts"; export class Migration674StableHomebrewTagIDs extends MigrationBase { static override version = 0.674; - private homebrewKeys = deepClone(HOMEBREW_TRAIT_KEYS); + #homebrewKeys = deepClone(HOMEBREW_TRAIT_KEYS); - private homebrewTags = this.homebrewKeys.reduce( + #homebrewTags = this.#homebrewKeys.reduce( (settings, key) => mergeObject(settings, { [key]: game.settings.get("pf2e", `homebrew.${key}`) }), - {} as Record<(typeof this.homebrewKeys)[number], HomebrewTag[]>, + {} as Record<(typeof HOMEBREW_TRAIT_KEYS)[number], HomebrewTag[]>, ); - private updateDocumentTags(documentTags: string[] = []): void { - for (const key of this.homebrewKeys) { - const homebrewTags = this.homebrewTags[key]; + #updateDocumentTags(documentTags: string[] = []): void { + for (const key of this.#homebrewKeys) { + const homebrewTags = this.#homebrewTags[key]; for (const tag of homebrewTags) { const index = documentTags.indexOf(tag.id); if (index !== -1) documentTags.splice(index, 1, `hb_${sluggify(tag.value)}`); @@ -27,19 +27,19 @@ export class Migration674StableHomebrewTagIDs extends MigrationBase { override async updateActor(source: MaybeWithExtraNestedTraits): Promise { if (source.type === "familiar" || !source.system.traits?.traits) return; - this.updateDocumentTags(source.system.traits.traits.value); + this.#updateDocumentTags(source.system.traits.traits.value); if (source.type === "character" || source.type === "npc") { - this.updateDocumentTags(source.system.traits?.languages.value); + this.#updateDocumentTags(source.system.traits?.languages.value); } } - override async updateItem(itemSource: ItemSourcePF2e): Promise { - this.updateDocumentTags(itemSource.system.traits?.value); + override async updateItem(source: ItemSourcePF2e): Promise { + this.#updateDocumentTags(source.system.traits?.value); } override async migrate(): Promise { - for (const key of this.homebrewKeys) { - const tags: { id: string; value: string }[] = this.homebrewTags[key]; + for (const key of this.#homebrewKeys) { + const tags: { id: string; value: string }[] = this.#homebrewTags[key]; for (const tag of tags) { tag.id = `hb_${sluggify(tag.value)}`; diff --git a/src/module/migration/migrations/703-spell-damage-structure.ts b/src/module/migration/migrations/703-spell-damage-structure.ts index e8af587e856..92317fba28c 100644 --- a/src/module/migration/migrations/703-spell-damage-structure.ts +++ b/src/module/migration/migrations/703-spell-damage-structure.ts @@ -1,16 +1,18 @@ import { ItemSourcePF2e } from "@item/base/data/index.ts"; +import { isObject } from "@util"; import { MigrationBase } from "../base.ts"; /** Correct the structure of spell damage in case it slipped past a previous migration */ export class Migration703SpellDamageStructure extends MigrationBase { static override version = 0.703; - override async updateItem(itemSource: ItemSourcePF2e): Promise { - if (itemSource.type === "spell") { - if (!(itemSource.system.damage instanceof Object)) { - itemSource.system.damage = { value: {} }; - } else if (!(itemSource.system.damage.value instanceof Object)) { - itemSource.system.damage.value = {}; + override async updateItem(source: ItemSourcePF2e): Promise { + if (source.type === "spell") { + const system: { damage: unknown } = source.system; + if (!isObject>(system.damage)) { + system.damage = { value: {} }; + } else if (!("value" in system.damage) || !isObject(system.damage.value)) { + system.damage.value = {}; } } } diff --git a/src/module/migration/migrations/747-fixed-heightening.ts b/src/module/migration/migrations/747-fixed-heightening.ts index 459fb1d2cd7..a334a44f4ae 100644 --- a/src/module/migration/migrations/747-fixed-heightening.ts +++ b/src/module/migration/migrations/747-fixed-heightening.ts @@ -1,6 +1,6 @@ -import { SpellPF2e } from "@item"; +import type { SpellPF2e } from "@item"; import { ItemSourcePF2e, SpellSource } from "@item/base/data/index.ts"; -import { sluggify } from "@util"; +import { isObject, sluggify } from "@util"; import { UUIDUtils } from "@util/uuid.ts"; import { MigrationBase } from "../base.ts"; @@ -29,10 +29,12 @@ export class Migration747FixedHeightening extends MigrationBase { const newDamage = newSpell.system.damage; const newKeys = new Set(Object.keys(newDamage.value)); const diff = Object.keys(spell.system.damage.value).filter((key) => !newKeys.has(key)); - const damage: { value: Record } = spell.system.damage; + const damage: Record | { value: Record } = spell.system.damage; damage.value = newDamage.value; for (const deleteKey of diff) { - damage.value[`-=${deleteKey}`] = null; + if (isObject>(damage.value)) { + damage.value[`-=${deleteKey}`] = null; + } } } diff --git a/src/module/migration/migrations/829-rm-ritual-entries.ts b/src/module/migration/migrations/829-rm-ritual-entries.ts index 34c4f1be0c5..fbc87d69d0e 100644 --- a/src/module/migration/migrations/829-rm-ritual-entries.ts +++ b/src/module/migration/migrations/829-rm-ritual-entries.ts @@ -2,6 +2,7 @@ import { ActorSourcePF2e } from "@actor/data/index.ts"; import { ItemSourcePF2e } from "@item/base/data/index.ts"; import { SpellcastingEntrySource } from "@item/spellcasting-entry/index.ts"; import { MigrationBase } from "../base.ts"; +import { isObject } from "@util"; /** Remove ritual spellcasting entries */ export class Migration829RMRitualEntries extends MigrationBase { @@ -23,6 +24,13 @@ export class Migration829RMRitualEntries extends MigrationBase { } override async updateItem(source: ItemSourcePF2e): Promise { + if ( + !("category" in source.system) || + !isObject<{ value: unknown }>(source.system.category) || + typeof source.system.category.value !== "string" + ) { + return; + } if (source.type === "spell" && source.system.category.value === "ritual" && source.system.location) { source.system.location.value = null; } diff --git a/src/module/migration/migrations/846-spell-school-optional.ts b/src/module/migration/migrations/846-spell-school-optional.ts index 9e0b9104bf1..788ad8de2e5 100644 --- a/src/module/migration/migrations/846-spell-school-optional.ts +++ b/src/module/migration/migrations/846-spell-school-optional.ts @@ -1,7 +1,7 @@ import { ItemSourcePF2e } from "@item/base/data/index.ts"; -import { MigrationBase } from "../base.ts"; -import { MagicSchool, SpellSystemSource } from "@item/spell/index.ts"; +import { SpellSystemSource } from "@item/spell/index.ts"; import * as R from "remeda"; +import { MigrationBase } from "../base.ts"; // Spell schools are gone as of remaster and rage of elements. Convert to trait for old spells export class Migration846SpellSchoolOptional extends MigrationBase { @@ -12,7 +12,8 @@ export class Migration846SpellSchoolOptional extends MigrationBase { const system: SpellSystemSourceWithSchool = source.system; if (system.school) { - source.system.traits.value = R.uniq(R.compact([...source.system.traits.value, system.school.value])); + const traits: { value: string[] } = system.traits; + traits.value = R.uniq(R.compact([...source.system.traits.value, system.school.value])); system["-=school"] = null; delete system.school; } @@ -20,6 +21,6 @@ export class Migration846SpellSchoolOptional extends MigrationBase { } interface SpellSystemSourceWithSchool extends SpellSystemSource { - school?: { value: MagicSchool }; + school?: { value: string }; "-=school"?: null; } diff --git a/src/module/migration/migrations/882-spell-data-reorganization.ts b/src/module/migration/migrations/882-spell-data-reorganization.ts new file mode 100644 index 00000000000..b319cfdbd73 --- /dev/null +++ b/src/module/migration/migrations/882-spell-data-reorganization.ts @@ -0,0 +1,270 @@ +import { SaveType } from "@actor/types.ts"; +import { SAVE_TYPES } from "@actor/values.ts"; +import { ItemSourcePF2e } from "@item/base/data/index.ts"; +import { SpellDamageSource, SpellSystemSource } from "@item/spell/data.ts"; +import { MagicTradition, SpellTrait } from "@item/spell/types.ts"; +import { DamageCategoryUnique, DamageType, MaterialDamageEffect } from "@system/damage/types.ts"; +import { isObject, tupleHasValue } from "@util"; +import * as R from "remeda"; +import { MigrationBase } from "../base.ts"; +import { ActorSourcePF2e } from "@actor/data/index.ts"; + +/** Simplify data structure of spells, add damage `kinds` */ +export class Migration882SpellDataReorganization extends MigrationBase { + static override version = 0.882; + + #ensureTraitsPresence(system: MaybeOldSpellSystemSource): { value: SpellTrait[] } { + return mergeObject({ value: [] }, system.traits ?? { value: [] }); + } + + override async updateItem( + source: DeepPartial, + actorSource?: ActorSourcePF2e, + /** Whether this is the top level of the spell item rather than internal partial data */ + { topLevel = true } = {}, + ): Promise { + if (source.type !== "spell") return; + const system: MaybeOldSpellSystemSource = source.system ?? {}; + + // Flatten traditions object + if (isObject(system.traditions) && "value" in system.traditions && Array.isArray(system.traditions.value)) { + system.traits = this.#ensureTraitsPresence(system); + system.traits.traditions = [...system.traditions.value].sort(); + } + if ("traditions" in system) system["-=traditions"] = null; + + // Add focus trait if a focus spell + if (system.category?.value === "focus") { + const traits = (system.traits = this.#ensureTraitsPresence(system)); + traits.value.push("focus"); + } + + // Remove `components`, adding to traits as necessary + if (isObject(system.components)) { + if (system.components.verbal) { + const traits = (system.traits = this.#ensureTraitsPresence(system)); + traits.value.push("concentrate"); + } + if (system.components.material || system.components.somatic) { + const traits = (system.traits = this.#ensureTraitsPresence(system)); + traits.value.push("manipulate"); + } + } + if ("components" in system) system["-=components"] = null; + + // Move `materials` to `requirements` + if (isObject(system.materials) && typeof system.materials.value === "string") { + system.requirements = system.materials.value; + } else if (topLevel) { + system.requirements ||= ""; + } + if ("materials" in system) system["-=materials"] = null; + + // Move `sustained` to under `duration` + if (topLevel) { + system.duration = { + value: system.duration?.value ?? "", + sustained: isObject(system.sustained) + ? !!system.sustained.value || system.duration?.value?.includes("sustained") || false + : system.duration?.sustained ?? false, + }; + } + if ("sustained" in system) system["-=sustained"] = null; + + // Shorten `hasCounteractCheck.value` to `counteracts` + if (isObject(system.hasCounteractCheck)) { + system.counteraction = !!system.hasCounteractCheck.value; + } else if (topLevel) { + system.counteraction ??= false; + } + if ("hasCounteractCheck" in system) { + system["-=hasCounteractCheck"] = null; + } + + // Replace `save` with `defense` + if ( + isObject(system.save) && + typeof system.save?.value === "string" && + tupleHasValue(SAVE_TYPES, system.save.value) + ) { + system.defense = { + save: { statistic: system.save.value, basic: !!system.save.basic }, + }; + } + if (topLevel) system.defense ??= null; + if ("save" in system) system["-=save"] = null; + + // Flatten `damage` object + const oldSpellDamage: Record = system.damage ?? {}; + const damage = deepClone( + isObject(system.damage) && "value" in system.damage ? system.damage.value : system.damage ?? {}, + ) as Record & Record<`-=${string}`, null | undefined>; + system.damage = topLevel || !!system.damage ? (damage as Record) : undefined; + + for (const [key, oldValue] of Object.entries(oldSpellDamage)) { + if (key === "value" || !isObject(oldValue)) damage[`-=${key}`] = null; + } + + for (const damagePartial of Object.values(damage)) { + if (!isObject(damagePartial)) continue; + + if ("value" in damagePartial && typeof damagePartial.value === "string") { + damagePartial.formula = damagePartial.value; + delete damagePartial.value; + } + + if (isObject(damagePartial.type)) { + const oldTypeData = damagePartial.type; + damagePartial.type = oldTypeData.value; + damagePartial.category = oldTypeData.subtype || null; + damagePartial.materials = oldTypeData.categories; + } + } + + // Remove `spellType`, adding to damage kinds if healing + if (isObject(system.spellType)) { + if (system.spellType.value === "attack" && !system.traits?.value?.includes("attack")) { + const traits = (system.traits = this.#ensureTraitsPresence(system)); + traits.value.push("attack"); + } else if (system.spellType.value === "heal") { + for (const damagePartial of Object.values(system.damage ?? {})) { + if (damagePartial) damagePartial.kinds = ["healing"]; + } + } + + for (const damagePartial of Object.values(system.damage ?? {})) { + if (damagePartial) damagePartial.kinds ??= ["damage"]; + } + } + if ("spellType" in system) system["-=spellType"] = null; + + // Remove `category`, setting up ritual data if necessary + if (isObject(system.category) && "value" in system.category && system.category.value === "ritual") { + const primaryCheck = + isObject(system.primarycheck) && typeof system.primarycheck.value === "string" + ? system.primarycheck.value.trim() + : ""; + const secondaryChecks = + isObject(system.secondarycheck) && typeof system.secondarycheck.value === "string" + ? system.secondarycheck.value.trim() + : ""; + const secondaryCasters = isObject(system.secondarycasters) ? Number(system.secondarycasters.value) || 0 : 0; + system.ritual = { + primary: { check: primaryCheck }, + secondary: { checks: secondaryChecks, casters: secondaryCasters }, + }; + } + if ("category" in system) system["-=category"] = null; + if ("primarycheck" in system) system["-=primarycheck"] = null; + if ("secondarycheck" in system) system["-=secondarycheck"] = null; + if ("secondarycasters" in system) system["-=secondarycasters"] = null; + + // Remove school traits from aura REs + for (const rule of source.system?.rules ?? []) { + if (isObject(rule) && "traits" in rule && Array.isArray(rule.traits)) { + rule.traits = rule.traits.filter((t) => !schools.includes(t)); + } + } + + // Final traits cleanup + const schools = ["conjuration", "divination", "enchantment", "evocation", "necromancy", "transmutation"]; + if (system.traits?.value && Array.isArray(system.traits.value)) { + system.traits.value = R.uniq( + R.compact(system.traits.value) + .filter((t: string) => !schools.includes(t)) + .sort(), + ); + } + + // Remove random legacy cruft + const oldKeys = ["ability", "areatype", "damageType", "prepared", "rarity", "spellCategorie", "usage"] as const; + for (const key of oldKeys) { + if (key in system) system[`-=${key}`] = null; + } + + // Repeat for heightening and overlays + if (isObject(system.heightening) && system.heightening.type === "fixed") { + for (const spellPartial of Object.values(system.heightening.levels ?? {})) { + await this.updateItem({ name: source.name, type: "spell", system: spellPartial }, actorSource, { + topLevel: false, + }); + } + } + + for (const overlay of Object.values(system?.overlays ?? {})) { + if (overlay.overlayType === "override") { + await this.updateItem( + { + name: overlay.name ?? source.name, + type: "spell", + system: overlay.system ?? {}, + }, + actorSource, + { topLevel: false }, + ); + } + } + } +} + +type MaybeOldSpellSystemSource = Omit, "traditions"> & { + spellType?: { value?: string }; + "-=spellType"?: null; + + category?: { value: string }; + "-=category"?: null; + + traditions?: { value?: MagicTradition[] }; + "-=traditions"?: null; + + components?: Record; + "-=components"?: null; + + materials?: { value?: unknown }; + "-=materials"?: null; + + sustained?: { value?: unknown }; + "-=sustained"?: null; + + save?: OldSaveData; + "-=save"?: null; + + primarycheck?: { value: unknown }; + "-=primarycheck"?: null; + secondarycheck?: { value: unknown }; + "-=secondarycheck"?: null; + secondarycasters?: { value: unknown }; + "-=secondarycasters"?: null; + + hasCounteractCheck?: { value?: unknown }; + "-=hasCounteractCheck"?: null; + + damage?: Record | { value?: Record }; + + // Random legacy cruft + "-=ability"?: null; + "-=areatype"?: null; + "-=damageType"?: null; + "-=prepared"?: null; + "-=rarity"?: null; + "-=spellCategorie"?: null; + "-=usage"?: null; +}; + +type SpellDamagePartialWithOldData = Omit & { + type: DamageType | OldSpellDamageType; + value?: string; +}; + +interface OldSpellDamageType { + value: DamageType; + subtype?: DamageCategoryUnique; + categories: MaterialDamageEffect[]; +} + +interface OldSaveData { + basic: string; + value: SaveType | ""; + dc?: number; + str?: string; +} diff --git a/src/module/migration/migrations/index.ts b/src/module/migration/migrations/index.ts index 59ca2b5797a..a3494f03daa 100644 --- a/src/module/migration/migrations/index.ts +++ b/src/module/migration/migrations/index.ts @@ -279,3 +279,4 @@ export { Migration878TakeABreather } from "./878-take-a-breather.ts"; export { Migration879DeviseAStratagemAndFriends } from "./879-devise-a-stratagem-and-friends.ts"; export { Migration880SplitShowDialogsSettings } from "./880-split-show-dialogs-setting.ts"; export { Migration881NoHBPrefix } from "./881-no-hb-prefix.ts"; +export { Migration882SpellDataReorganization } from "./882-spell-data-reorganization.ts"; diff --git a/src/module/migration/runner/base.ts b/src/module/migration/runner/base.ts index 988c2be3b82..bf67036cc70 100644 --- a/src/module/migration/runner/base.ts +++ b/src/module/migration/runner/base.ts @@ -13,7 +13,7 @@ interface CollectionDiff; type DamageCategory = keyof typeof CONFIG.PF2E.damageCategories; type DamageDieSize = SetElement; type DamageType = SetElement; +type DamageKind = "damage" | "healing"; type MaterialDamageEffect = keyof typeof CONFIG.PF2E.materialDamageEffects; /** @@ -19,8 +20,6 @@ type MaterialDamageEffect = keyof typeof CONFIG.PF2E.materialDamageEffects; */ type CriticalInclusion = boolean | null; -type DamageKind = "damage" | "healing"; - interface DamageCategoryRenderData { dice: { faces: number; diff --git a/src/scripts/config/index.ts b/src/scripts/config/index.ts index aea3b6b26f6..79900180254 100644 --- a/src/scripts/config/index.ts +++ b/src/scripts/config/index.ts @@ -55,7 +55,6 @@ import { featTraits, hazardTraits, kingmakerTraits, - magicSchools, magicTraditions, npcAttackTraits, otherArmorTags, @@ -601,7 +600,6 @@ export const PF2ECONFIG = { }, magicTraditions, - magicSchools, classTraits, ancestryTraits, ancestryItemTraits, @@ -792,19 +790,6 @@ export const PF2ECONFIG = { F: "PF2E.SpellComponentF", }, - spellCategories: { - spell: "PF2E.SpellCategorySpell", - focus: "PF2E.SpellCategoryFocus", - ritual: "PF2E.SpellCategoryRitual", - }, - - spellTypes: { - attack: "PF2E.SpellTypeAttack", - save: "PF2E.SpellTypeSave", - heal: "PF2E.SpellTypeHeal", - utility: "PF2E.SpellTypeUtility", - }, - featCategories, actionTypes: { diff --git a/src/scripts/config/iwr.ts b/src/scripts/config/iwr.ts index abdbff2ab3a..fe6792a327c 100644 --- a/src/scripts/config/iwr.ts +++ b/src/scripts/config/iwr.ts @@ -27,7 +27,6 @@ const immunityTypes = { clumsy: "PF2E.Damage.IWR.Type.clumsy", cold: "PF2E.Damage.RollFlavor.cold", confused: "PF2E.Damage.IWR.Type.confused", - conjuration: "PF2E.Damage.IWR.Type.conjuration", controlled: "PF2E.Damage.IWR.Type.controlled", "critical-hits": "PF2E.Damage.IWR.Type.critical-hits", curse: "PF2E.Damage.IWR.Type.curse", @@ -42,11 +41,9 @@ const immunityTypes = { earth: "PF2E.Damage.RollFlavor.earth", electricity: "PF2E.Damage.RollFlavor.electricity", emotion: "PF2E.Damage.IWR.Type.emotion", - enchantment: "PF2E.Damage.IWR.Type.enchantment", energy: "PF2E.Damage.IWR.Type.energy", enfeebled: "PF2E.Damage.IWR.Type.enfeebled", evil: "PF2E.Damage.RollFlavor.evil", - evocation: "PF2E.Damage.IWR.Type.evocation", fascinated: "PF2E.Damage.IWR.Type.fascinated", fatigued: "PF2E.Damage.IWR.Type.fatigued", "fear-effects": "PF2E.Damage.IWR.Type.fear-effects", @@ -66,7 +63,6 @@ const immunityTypes = { mental: "PF2E.Damage.RollFlavor.mental", metal: "PF2E.Damage.IWR.Type.metal", "misfortune-effects": "PF2E.Damage.IWR.Type.misfortune-effects", - necromancy: "PF2E.Damage.IWR.Type.necromancy", "non-magical": "PF2E.Damage.IWR.Type.non-magical", "nonlethal-attacks": "PF2E.Damage.IWR.Type.nonlethal-attacks", "object-immunities": "PF2E.Damage.IWR.Type.object-immunities", @@ -98,7 +94,6 @@ const immunityTypes = { stupefied: "PF2E.Damage.IWR.Type.stupefied", "swarm-attacks": "PF2E.Damage.IWR.Type.swarm-attacks", "swarm-mind": "PF2E.Damage.IWR.Type.swarm-mind", - transmutation: "PF2E.Damage.IWR.Type.transmutation", trip: "PF2E.Damage.IWR.Type.trip", "unarmed-attacks": "PF2E.Damage.IWR.Type.unarmed-attacks", unconscious: "PF2E.Damage.IWR.Type.unconscious", diff --git a/src/scripts/config/traits.ts b/src/scripts/config/traits.ts index f15602649fb..c53eeeb6ff4 100644 --- a/src/scripts/config/traits.ts +++ b/src/scripts/config/traits.ts @@ -5,7 +5,7 @@ import { RANGE_TRAITS } from "@item/base/data/values.ts"; import { ClassTrait } from "@item/class/types.ts"; import { OtherConsumableTag } from "@item/consumable/types.ts"; import { PreciousMaterialType } from "@item/physical/types.ts"; -import { MagicSchool, MagicTradition } from "@item/spell/types.ts"; +import { MagicTradition } from "@item/spell/types.ts"; import { OtherWeaponTag } from "@item/weapon/types.ts"; import { sluggify } from "@util"; import * as R from "remeda"; @@ -160,7 +160,6 @@ const creatureTraits = { eidolon: "PF2E.TraitEidolon", elemental: "PF2E.TraitElemental", ethereal: "PF2E.TraitEthereal", - evocation: "PF2E.TraitEvocation", fiend: "PF2E.TraitFiend", formian: "PF2E.TraitFormian", fungus: "PF2E.TraitFungus", @@ -200,7 +199,6 @@ const creatureTraits = { munavri: "PF2E.TraitMunavri", mutant: "PF2E.TraitMutant", nagaji: "PF2E.TraitNagaji", - necromancy: "PF2E.TraitNecromancy", nephilim: "PF2E.TraitNephilim", nindoru: "PF2E.TraitNindoru", nymph: "PF2E.TraitNymph", @@ -314,23 +312,11 @@ const damageTraits = { void: "PF2E.TraitVoid", }; -const magicSchools: Record = { - abjuration: "PF2E.TraitAbjuration", - conjuration: "PF2E.TraitConjuration", - divination: "PF2E.TraitDivination", - enchantment: "PF2E.TraitEnchantment", - evocation: "PF2E.TraitEvocation", - illusion: "PF2E.TraitIllusion", - necromancy: "PF2E.TraitNecromancy", - transmutation: "PF2E.TraitTransmutation", -}; - const spellTraits = { ...alignmentTraits, ...classTraits, ...damageTraits, ...elementTraits, - ...magicSchools, ...magicTraditions, ...sanctificationTraits, amp: "PF2E.TraitAmp", @@ -354,10 +340,12 @@ const spellTraits = { emotion: "PF2E.TraitEmotion", extradimensional: "PF2E.TraitExtradimensional", fear: "PF2E.TraitFear", + focus: "PF2E.TraitFocus", fortune: "PF2E.TraitFortune", fungus: "PF2E.TraitFungus", healing: "PF2E.TraitHealing", hex: "PF2E.TraitHex", + illusion: "PF2E.TraitIllusion", incapacitation: "PF2E.TraitIncapacitation", incarnate: "PF2E.TraitIncarnate", incorporeal: "PF2E.TraitIncorporeal", @@ -398,7 +386,6 @@ const weaponTraits = { ...ancestryTraits, ...elementTraits, ...energyDamageTypes, - ...magicSchools, ...magicTraditions, ...sanctificationTraits, adjusted: "PF2E.TraitAdjusted", @@ -611,7 +598,6 @@ const featTraits = { ...ancestryTraits, ...classTraits, ...damageTraits, - ...magicSchools, ...magicTraditions, ...spellTraits, additive1: "PF2E.TraitAdditive1", @@ -763,7 +749,6 @@ const actionTraits = { const hazardTraits = { ...damageTraits, - ...magicSchools, ...magicTraditions, aberration: "PF2E.TraitAberration", alchemical: "PF2E.TraitAlchemical", @@ -794,7 +779,6 @@ const hazardTraits = { }; const vehicleTraits = { - ...magicSchools, artifact: "PF2E.TraitArtifact", clockwork: "PF2E.TraitClockwork", magical: "PF2E.TraitMagical", @@ -806,7 +790,6 @@ const equipmentTraits = { ...ancestryTraits, ...elementTraits, ...energyDamageTypes, - ...magicSchools, ...magicTraditions, ...sanctificationTraits, additive0: "PF2E.TraitAdditive0", @@ -901,7 +884,6 @@ const armorTraits = { ...alignmentTraits, ...sanctificationTraits, ...elementTraits, - ...magicSchools, ...magicTraditions, ...shieldTraits, adjusted: "PF2E.TraitAdjusted", @@ -1057,7 +1039,6 @@ const traitDescriptions = { concealable: "PF2E.TraitDescriptionConcealable", concentrate: "PF2E.TraitDescriptionConcentrate", concussive: "PF2E.TraitDescriptionConcussive", - conjuration: "PF2E.TraitDescriptionConjuration", conrasu: "PF2E.TraitDescriptionConrasu", consecration: "PF2E.TraitDescriptionConsecration", consumable: "PF2E.TraitDescriptionConsumable", @@ -1108,13 +1089,11 @@ const traitDescriptions = { elf: "PF2E.TraitDescriptionElf", elixir: "PF2E.TraitDescriptionElixir", emotion: "PF2E.TraitDescriptionEmotion", - enchantment: "PF2E.TraitDescriptionEnchantment", "entrench-melee": "PF2E.TraitDescriptionEntrench", "entrench-ranged": "PF2E.TraitDescriptionEntrench", environment: "PF2E.TraitDescriptionEnvironment", esoterica: "PF2E.TraitDescriptionEsoterica", evil: "PF2E.TraitDescriptionEvil", - evocation: "PF2E.TraitDescriptionEvocation", evolution: "PF2E.TraitDescriptionEvolution", expandable: "PF2E.TraitDescriptionExpandable", exploration: "PF2E.TraitDescriptionExploration", @@ -1135,6 +1114,7 @@ const traitDescriptions = { fleshwarp: "PF2E.TraitDescriptionFleshwarp", flexible: "PF2E.TraitDescriptionFlexible", flourish: "PF2E.TraitDescriptionFlourish", + focus: "PF2E.TraitDescriptionFocus", focused: "PF2E.TraitDescriptionFocused", foldaway: "PF2E.TraitDescriptionFoldaway", force: "PF2E.TraitDescriptionForce", @@ -1231,7 +1211,6 @@ const traitDescriptions = { multiclass: "PF2E.TraitDescriptionMulticlass", mutagen: "PF2E.TraitDescriptionMutagen", nagaji: "PF2E.TraitDescriptionNagaji", - necromancy: "PF2E.TraitDescriptionNecromancy", nephilim: "PF2E.TraitDescriptionNephilim", nindoru: "PF2E.TraitDescriptionNindoru", noisy: "PF2E.TraitDescriptionNoisy", @@ -1363,7 +1342,6 @@ const traitDescriptions = { tiefling: "PF2E.TraitDescriptionTiefling", time: "PF2E.TraitDescriptionTime", training: "PF2E.TraitDescriptionTraining", - transmutation: "PF2E.TraitDescriptionTransmutation", trap: "PF2E.TraitDescriptionTrap", trip: "PF2E.TraitDescriptionTrip", "true-name": "PF2E.TraitDescriptionTrueName", @@ -1430,7 +1408,6 @@ export { featTraits, hazardTraits, kingmakerTraits, - magicSchools, magicTraditions, npcAttackTraits, otherArmorTags, diff --git a/src/styles/item/_index.scss b/src/styles/item/_index.scss index 26c61fc9d4e..b4a89d63319 100644 --- a/src/styles/item/_index.scss +++ b/src/styles/item/_index.scss @@ -312,6 +312,7 @@ padding-right: 0.5em; white-space: nowrap; + &.disabled, &.no-data { color: var(--color-text-dark-4); } @@ -478,22 +479,6 @@ } } - .toggle-button-list { - display: flex; - gap: 2px; - margin: 4px 0; - - button { - margin: 0; - font-size: var(--font-size-10); - line-height: 16px; - display: flex; - align-items: center; - white-space: nowrap; - padding: 3px 4px; - } - } - .consumable-details { flex: 0 0 24px; diff --git a/src/styles/item/_spell-sheet.scss b/src/styles/item/_spell-sheet.scss index 1f46fd7c8ca..7db5bcb2622 100644 --- a/src/styles/item/_spell-sheet.scss +++ b/src/styles/item/_spell-sheet.scss @@ -1,3 +1,9 @@ +.save-basic { + display: flex; + flex-wrap: nowrap; + justify-content: end; +} + .damage-formulas { margin-top: 8px; @@ -14,20 +20,44 @@ display: flex; margin-top: 2px; } - - .tag { - float: none; // remove when global rule is removed - } -} - -button[data-action="damage-add"] { - margin-top: 4px; } button + fieldset { margin-top: 0.5rem; } +fieldset.heightening { + display: flex; + flex-direction: column; + gap: 0.25rem; + + .add { + display: grid; + grid-template-columns: 1fr 1fr; + + button.only-option { + grid-column: span 2; + } + } + + .overlay { + .toggle-button-list { + column-gap: 2px; + display: grid; + grid-template-columns: repeat(6, 1fr); + margin: 4px 0; + + button { + font-size: var(--font-size-12); + line-height: normal; + margin: 0; + padding: 0.25em; + white-space: nowrap; + } + } + } +} + .overlay { .traits { align-items: center; diff --git a/static/lang/en.json b/static/lang/en.json index 278859b49b9..6abee99ba35 100644 --- a/static/lang/en.json +++ b/static/lang/en.json @@ -100,7 +100,7 @@ }, "ActionActionTypeLabel": "Action Type", "ActionActionsLabel": "Actions", - "ActionBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, school, components, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", + "ActionBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", "ActionDeathNoteLabel": "Death Note", "ActionNumber1": "One", "ActionNumber2": "Two", @@ -358,7 +358,7 @@ "Identification": { "Skills": { "Label": "{skills}: DC {dc} ({adjustment})", - "Tooltip": "\"The skill used to identify a creature usually depends on that creature’s trait, as shown on Table 10–7, but you have leeway on which skills apply.\" – CRB pg. 506" + "Tooltip": "\"The skill used to identify a creature usually depends on that creature's trait, as shown on Table 10–7, but you have leeway on which skills apply.\" – CRB pg. 506" }, "Lore": { "Label": "Applicable Lore: DC {dc1} ({adjustment1}) or DC {dc2} ({adjustment2})", @@ -618,7 +618,7 @@ "BackgroundSkillFeats": "Skill Feats", "BaseModifier": "Base Modifier", "BaseWeapons": "Base Weapons", - "BestiaryBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, school, components, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", + "BestiaryBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", "Biography": "Biography", "BiographyAllies": "Allies", "BiographyAppearance": "Appearance", @@ -664,7 +664,6 @@ "BrowserFilterLevels": "Levels", "BrowserFilterProficiencyRequirements": "Proficiency Requirements", "BrowserFilterRarities": "Rarities", - "BrowserFilterSchools": "Schools", "BrowserFilterSizes": "Sizes", "BrowserFilterSkills": "Skills", "BrowserFilterSource": "Source", @@ -1016,7 +1015,6 @@ "Exceptions4DoubleVs4": "{type} {value} (except {exception1}, {exception2}, {exception3}, or {exception4}; double resistance vs. {doubleVs1}, {doubleVs2}, {doubleVs3}, or {doubleVs4})" }, "Type": { - "abjuration": "abjuration", "abysium": "abysium", "adamantine": "adamantine", "all-damage": "all damage", @@ -1029,7 +1027,6 @@ "clumsy": "clumsy", "cold-iron": "cold iron", "confused": "confused", - "conjuration": "conjuration", "controlled": "controlled", "critical-hits": "critical hits", "curse": "curse", @@ -1039,15 +1036,12 @@ "deafened": "deafened", "death-effects": "death effects", "disease": "disease", - "divination": "divination", "djezet": "djezet", "doomed": "doomed", "drained": "drained", "emotion": "emotion", - "enchantment": "enchantment", "energy": "energy", "enfeebled": "enfeebled", - "evocation": "evocation", "fascinated": "fascinated", "fatigued": "fatigued", "fear-effects": "fear effects", @@ -1068,7 +1062,6 @@ "metal": "metal", "misfortune-effects": "misfortune effects", "mithral": "mithral", - "necromancy": "necromancy", "non-magical": "non-magical", "nonlethal": "nonlethal", "nonlethal-attacks": "nonlethal attacks", @@ -1103,7 +1096,6 @@ "stupefied": "stupefied", "swarm-attacks": "swarm attacks", "swarm-mind": "swarm mind", - "transmutation": "transmutation", "trip": "trip", "unarmed-attacks": "unarmed attacks", "unconscious": "unconscious", @@ -1389,7 +1381,7 @@ "FeatArchetypeHeader": "Archetype Feats", "FeatBackgroundShort": "BG", "FeatBonusHeader": "Bonus Feats", - "FeatBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, school, components, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", + "FeatBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", "FeatCampaignHeader": "Campaign Feats", "FeatClassHeader": "Class Feats", "FeatDeityBoonCursesHeader": "Divine Intercessions", @@ -1491,7 +1483,7 @@ "InventoryEquipmentHeader": "Equipment", "InventoryTreasureHeader": "Treasure", "InventoryWeaponsHeader": "Weapons", - "InventroyBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, school, components, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", + "InventroyBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", "InvestedLabel": "Invested", "Item": { "ABC": { @@ -2162,6 +2154,8 @@ "SidebarSummary": "{type} Summary", "Spell": { "Area": "{size}-{unit} {shape}", + "Cast": "Cast", + "Cost": "Cost", "Counteract": { "Hint": "If you're counteracting an affliction, the DC is in the affliction's stat block. If it's a spell, use the caster's DC. The GM can also calculate a DC based on the target effect's level.", "Label": "Counteract", @@ -2170,6 +2164,9 @@ "failure": "Counteract the target if its counteract rank is less than {rank}.", "success": "Counteract the target if its counteract rank is {rank} or less." }, + "Defense": { + "Label": "Defense" + }, "LevelN": "Spell {level}", "MeasuredTemplate": { "Clear": "Clear Placed Templates", @@ -2179,6 +2176,12 @@ "Label": "Rank", "Ordinal": "{rank} Rank" }, + "Ritual": { + "Label": "Ritual", + "PrimaryCheck": "Primary Check", + "SecondaryChecks": "Secondary Checks", + "SecondaryCasters": "Secondary Casters" + }, "Variants": { "DeleteDialogText": "Are you sure you want to delete '{variantName}'?", "DeleteDialogTitle": "Delete Spell Variant", @@ -3248,6 +3251,7 @@ "SavesReflexShort": "Ref", "SavesWill": "Will", "SavesWillShort": "Will", + "SavingThrow": "Saving Throw", "SavingThrowWithName": "{saveName} Saving Throw", "Scene": { "HearingRange": { @@ -3331,43 +3335,25 @@ "SpellAttackLabel": "Spell Attack", "SpellAttackWithTradition": "{tradition} Spell Attack", "SpellBasicSaveOption": "Basic", - "SpellBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, school, components, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", + "SpellBrowserSearchHint": "You can search for name or custom attributes. Possible searchable attributes are:
source, spellType, level, materials, target, range, time, duration, damage, damageType, save, concentration, ritual, ability and classes.
Example: 'fire, damage:d6' to show all spells that have fire in their name and a d6 in the damage", "SpellCastingFormat": "{traditionSpells} {preparationType} Spells", - "SpellCategoryFocus": "Focus", "SpellCategoryFocusCantrip": "Focus Cantrip", - "SpellCategoryLabel": "Spell Category", - "SpellCategoryRitual": "Ritual", "SpellCategorySpell": "Spell", "SpellCollectionAdd": "Add to Spell Collection", "SpellCollectionRemove": "Remove from Spell Collection", - "SpellComponentF": "Focus", - "SpellComponentM": "Material", - "SpellComponentS": "Somatic", - "SpellComponentShortF": "F", - "SpellComponentShortM": "M", - "SpellComponentShortS": "S", - "SpellComponentShortV": "V", - "SpellComponentV": "Verbal", - "SpellComponentsLabel": "Components", - "SpellConcentrationLabel": "Concentration", - "SpellCostLabel": "Cost", "SpellDCBase": "Base Spell DC 10", "SpellDamageLabel": "Spell Damage", "SpellDurationLabel": "Duration", "SpellFlexibleAvailable": "Flexible: {value} / {max}", "SpellFlexibleLabel": "Flexible", "SpellFocusLabel": "Focus", - "SpellLabel": "Spell", "SpellLabelPlural": "Spells", "SpellLevelBase": "Base: {level}", "SpellLevelHeightened": "Heightened: +{heightened}", "SpellLevelLabel": "Spell Rank", "SpellPreparedLabel": "Prepared", - "SpellPrimaryCheckLabel": "Primary Check", "SpellRangeLabel": "Range", "SpellRequirementsLabel": "Requirements", - "SpellRitualLabel": "Ritual", - "SpellSaveLabel": "Spell DC", "SpellScalingInterval": { "Add": "Add Heightening (Interval)", "Header": "Scaling (Interval)", @@ -3377,25 +3363,13 @@ "SpellScalingLabel": "Scaling", "SpellScalingOverlay": { "Add": "Add Heightening (Fixed)", - "Label": "Heightened Level", "Selection": "Heightened ({level})" }, - "SpellSchoolLabel": "School", - "SpellSecondaryCasters": "Secondary Casters", - "SpellSecondaryChecksLabel": "Secondary Checks", "SpellSlotEmpty": "Empty Slot (drag spell here)", "SpellSlotExpendedError": "Cannot cast {name}: spell is already expended", "SpellSlotNotEnoughError": "Cannot cast {name} at the {level}: not enough spell slots", "SpellTargetLabel": "Targets", - "SpellTimeLabel": "Cast Time", "SpellTraditionsLabel": "Traditions", - "SpellTypeAttack": "Spell Attack", - "SpellTypeFocus": "Focus", - "SpellTypeHeal": "Healing", - "SpellTypeLabel": "Spell Type", - "SpellTypeRitual": "Ritual", - "SpellTypeSave": "Saving Throw", - "SpellTypeUtility": "Utility", "SpellUnlimitedLabel": "Unlimited", "SpellUsesLabel": "Uses", "SpellcastingSettings": { @@ -3405,7 +3379,6 @@ }, "SpellcastingTypeLabel": "Spellcasting Type", "SpellsActionHeader": "Action", - "SpellsSchoolHeader": "School", "StackGroupArrows": "Arrows", "StackGroupBlowgunDarts": "Blowgun Darts", "StackGroupBolts": "Bolts", @@ -3515,7 +3488,6 @@ "TrainedSkillsLabel": "Trained Skills", "TraitAasimar": "Aasimar", "TraitAberration": "Aberration", - "TraitAbjuration": "Abjuration", "TraitAcid": "Acid", "TraitAdditive0": "Additive 0", "TraitAdditive1": "Additive 1", @@ -3661,7 +3633,6 @@ "TraitConcentrate": "Concentrate", "TraitConcussive": "Concussive", "TraitConfusion": "Confusion", - "TraitConjuration": "Conjuration", "TraitConrasu": "Conrasu", "TraitConsecration": "Consecration", "TraitConstruct": "Construct", @@ -3701,7 +3672,6 @@ "TraitDemon": "Demon", "TraitDero": "Dero", "TraitDescriptionAasimar": "A creature with this trait is a member of the aasimar ancestry.", - "TraitDescriptionAbjuration": "Effects and magic items with this trait are associated with the abjuration school of magic, typically involving protection or wards.", "TraitDescriptionAcid": "Effects with this trait deal acid damage. Creatures with this trait have a magical connection to acid.", "TraitDescriptionAdditive": "Feats with the additive trait allow you to spend actions to add special substances to bombs or elixirs. You can add only one additive to a single alchemical item, and attempting to add another spoils the item. You can typically use actions with the additive trait only when you're creating an infused alchemical item, and some can be used only with the Quick Alchemy action. The additive trait is always followed by a level, such as additive 2. An additive adds its level to the level of the alchemical item you're modifying; the result is the new level of the mixture. The mixture's item level must be no higher than your advanced alchemy level", "TraitDescriptionAdjusted": "The equipment comes with an adjustment described in its entry. This adjustment is built into the equipment permanently, meaning the equipment can't have another adjustment added, nor can it be swapped out for a different adjustment. If the adjustment alters the item's base statistics, such as adding the noisy trait, that's reflected in the equipment's table entry.", @@ -3762,12 +3732,11 @@ "TraitDescriptionCommon": "Anything that doesn't list another rarity trait (uncommon, rare, or unique) automatically has the common trait. This rarity indicates that an ability, item, or spell is available to all players who meet the prerequisites for it. A creature of this rarity is generally known and can be summoned with the appropriate summon spell.", "TraitDescriptionCompanion": "An item with this trait can be worn by an animal companion or similar creature. A companion can have up to two items invested.", "TraitDescriptionComplex": "A hazard with this trait takes turns in an encounter.", - "TraitDescriptionComposite": "A composite impulse combines multiple elements. You can gain an impulse with the composite trait only if your kinetic elements include all the elements listed in the impulse’s traits.", + "TraitDescriptionComposite": "A composite impulse combines multiple elements. You can gain an impulse with the composite trait only if your kinetic elements include all the elements listed in the impulse's traits.", "TraitDescriptionComposition": "To cast a composition cantrip or focus spell, you usually use a type of Performance. If the spell includes a verbal component, you must use an auditory performance, and if it includes a somatic component, you must use a visual one. The spell gains all the traits of the performance you used. You can cast only one composition spell each turn, and you can have only one active at a time. If you cast a new composition spell, any ongoing effects from your previous composition spell end immediately.", "TraitDescriptionConcealable": "This weapon is designed to be inconspicuous or easily concealed. You gain a +2 circumstance bonus to Stealth checks and DCs to hide or conceal a weapon with this trait.", "TraitDescriptionConcentrate": "An action with this trait requires a degree of mental concentration and discipline.", "TraitDescriptionConcussive": "These weapons smash as much as puncture. When determining a creature's resistance or immunity to damage from this weapon, use the weaker of the target's resistance or immunity to piercing or to bludgeoning. For instance, if the creature were immune to piercing and had no resistance or immunity to bludgeoning damage, it would take full damage from a concussive weapon. Resistance or immunity to all physical damage, or all damage, applies as normal.", - "TraitDescriptionConjuration": "Effects and magic items with this trait are associated with the conjuration school of magic, typically involving summoning, creation, teleportation, or moving things from place to place.", "TraitDescriptionConrasu": "A people that are made of cosmic force given consciousness and housed within unique exoskeletons.", "TraitDescriptionConsecration": "A consecration spell enhances an area for an extended period of time. A given area can have only a single consecration effect at a time. The new effect attempts to counteract any existing one in areas of overlap.", "TraitDescriptionConstruct": "A construct is an artificial creature empowered by a force other than necromancy. Constructs are often mindless; they are immune to bleed damage, death effects, disease, healing, necromancy, nonlethal attacks, poison, and the doomed, drained, fatigued, paralyzed, sickened, and unconscious conditions; and they may have Hardness based on the materials used to construct their bodies. Constructs are not living creatures, nor are they undead. When reduced to 0 Hit Points, a construct creature is destroyed.", @@ -3790,7 +3759,6 @@ "TraitDescriptionDhampir": "A creature with this trait is a member of the dhampir ancestry. These humanoids are the mortal offspring of vampires and members of other ancestries.", "TraitDescriptionDisarm": "You can use this weapon to Disarm with the Athletics skill even if you don't have a free hand. This uses the weapon's reach (if different from your own) and adds the weapon's item bonus to attack rolls (if any) as an item bonus to the Athletics check. If you critically fail a check to Disarm using the weapon, you can drop the weapon to take the effects of a failure instead of a critical failure. On a critical success, you still need a free hand if you want to take the item.", "TraitDescriptionDisease": "An effect with this trait applies one or more diseases. A disease is typically an affliction.", - "TraitDescriptionDivination": "The divination school of magic typically involves obtaining or transferring information, or predicting events.", "TraitDescriptionDivine": "This magic comes from the divine tradition, drawing power from deities or similar sources. Anything with this trait is magical.", "TraitDescriptionDoubleBarrel": "This weapon has two barrels that are each loaded separately. You can fire both barrels of a double barrel weapon in a single Strike to increase the weapon damage die by one step. If the weapon has the fatal trait, this increases the fatal die by one step.", "TraitDescriptionDowntime": "An activity with this trait takes a day or more, and can be used only during downtime.", @@ -3804,12 +3772,10 @@ "TraitDescriptionElf": "A creature with this trait is a member of the elf ancestry. Elves are mysterious people with rich traditions of magic and scholarship who typically have low-light vision. An ability with this trait can be used or selected only by elves. A weapon with this trait is created and used by elves.", "TraitDescriptionElixir": "Elixirs are alchemical liquids that are used by drinking them.", "TraitDescriptionEmotion": "This effect alters a creature's emotions. Effects with this trait always have the mental trait as well. Creatures with special training or that have mechanical or artificial intelligence are immune to emotion effects.", - "TraitDescriptionEnchantment": "Effects and magic items with this trait are associated with the enchantment school of magic, typically involving mind control, emotion alteration, and other mental effects.", "TraitDescriptionEntrench": "You can position yourself in the armor or reposition its articulated pieces to better protect against some attacks. If you're trained in this armor, while wearing it you can spend a single action to gain a +1 circumstance bonus to AC against a certain type of attack until the start of your next turn. The entrench trait lists the type of attack this bonus applies against, typically entrench melee or entrench ranged.", "TraitDescriptionEnvironment": "A hazard with this trait is something dangerous that's part of the natural world, such as quicksand or harmful mold.", "TraitDescriptionEsoterica": "The esoterica trait is present in many thaumaturge feats and class features that incorporate the various talismans, supernatural trinkets, and other objects you carry with you. Abilities that have the esoterica trait require you to be in possession of your esoterica to use them. Normally, you're assumed to always have your esoterica with you, but in some rare circumstances, you might either not have them on hand or have your gear stripped from you.", "TraitDescriptionEvil": "Evil effects often manipulate energy from evil-aligned Outer Planes and are antithetical to good divine servants or divine servants of good deities. A creature with this trait is evil in alignment.", - "TraitDescriptionEvocation": "Effects and magic items with this trait are associated with the evocation school of magic, typically involving energy and elemental forces.", "TraitDescriptionEvolution": "Feats with this trait affect your eidolon instead of you, typically by granting it additional physical capabilities.", "TraitDescriptionExpandable": "An item with the expandable trait increases to a specific size when activated. Unless otherwise noted, this space must be adjacent to you and on the ground, and the item needs to have enough open space to expand into or else the activation has no effect. When the effect ends, the expanded item disintegrates if it's a consumable or shrinks back to its normal size if it's not a consumable.", "TraitDescriptionExploration": "An activity with this trait takes more than a turn to use, and can usually be used only during exploration mode.", @@ -3828,6 +3794,7 @@ "TraitDescriptionFleshwarp": "A humanoid transformed so completely by outside forces that they are now a unique ancestry.", "TraitDescriptionFlexible": "The armor is flexible enough that it doesn't hinder most actions. You don't apply its check penalty to Acrobatics or Athletics checks.", "TraitDescriptionFlourish": "Flourish actions are actions that require too much exertion to perform a large number in a row. You can use only 1 action with the flourish trait per turn.", + "TraitDescriptionFocus": "Focus spells are a special type of spell attained directly from a branch of study, from a deity, or from another specific source. You can learn focus spells only through special class features or feats, rather than choosing them from a spell list. Furthermore, you cast focus spells using a special pool of Focus Points—you can't prepare a focus spell in a spell slot or use your spell slots to cast focus spells; similarly, you can't spend your Focus Points to cast spells that aren't focus spells.", "TraitDescriptionFocused": "An item with this trait can give you an additional Focus Point. This focus point is separate from your focus pool and doesn't count toward the cap on your focus pool. You can gain this benefit only if you have a focus pool, and there might be restrictions on how the point can be used. You can't gain more than 1 Focus Point per day from focused items.", "TraitDescriptionFoldaway": "

This shield can collapse into a smaller form attached to a gauntlet for stability and easy travel. You can use an Interact action to deploy or stow the shield. While it's deployed, you can Raise the Shield as long as you have that hand free or are holding an object of light Bulk that's not a weapon in that hand. The shield impedes the use of your hand while it's deployed. Though you can still hold items in that hand, you can't wield weapons in that hand, operate anything that takes two hands, or attack with the gauntlet.

A foldaway shield must be attached to a gauntlet for stability. It can be affixed to an item with 10 minutes of work and a successful DC 10 Crafting check; this includes the time needed to remove the shield from a previous gauntlet, if necessary. If the gauntlet is destroyed, the foldaway shield can usually be salvaged. The collapsible nature of the shield makes it impossible to affix an attached weapon to it.

", "TraitDescriptionForce": "Effects with this trait deal force damage or create objects made of pure magical force.", @@ -3866,7 +3833,7 @@ "TraitDescriptionHuman": "A creature with this trait is a member of the human ancestry. Humans are a diverse array of people known for their adaptability. An ability with this trait can be used or selected only by humans.", "TraitDescriptionHumanoid": "Humanoid creatures reason and act much like humans. They typically stand upright and have two arms and two legs.", "TraitDescriptionIfrit": "A type of geniekin descended from a being from the Plane of Fire.", - "TraitDescriptionIllusion": "Effects and magic items with this trait are associated with the illusion school of magic, typically involving false sensory stimuli.", + "TraitDescriptionIllusion": "Effects and magic items with this trait involve false sensory stimuli.", "TraitDescriptionImpulse": "The primary magical actions kineticists use are called impulses. You can use an impulse only if your kinetic aura is active and channeling that element, and only if you have a hand free to shape the elemental flow. The impulse trait means the action has the concentrate trait unless another ability changes this. If an impulse allows you to choose an element, you can choose any element you're channeling, and the impulse gains that element's trait.", "TraitDescriptionIncapacitation": "An ability with this trait can take a character completely out of the fight or even kill them, and it's harder to use on a more powerful character. If a spell has the incapacitation trait, any creature of more than twice the spell's rank treats the result of their check to prevent being incapacitated by the spell as one degree of success better, or the result of any check the spellcaster made to incapacitate them as one degree of success worse. If any other effect has the incapacitation trait, a creature of higher level than the item, creature, or hazard generating the effect gains the same benefits.", "TraitDescriptionIncarnate": "A spell with the incarnate trait is similar in theme to spells that summon creatures, but it doesn't conjure a minion with the summoned trait. Instead, when summoned, the incarnate creature takes its Arrive action when you finish Casting the Spell. At the end of your next turn, the incarnate creature can either Step, Stride, or take the action for another movement type it has (such as Climb or Burrow), and then takes its Depart action. The spell then ends. The names of specific Arrive and Depart actions are listed in italics after the word “Arrive” or “Depart” respectively, along with any traits. A creature summoned by an incarnate spell acts in your interests, directs its effects away from you and your allies as much as possible, and might listen to your requests, but ultimately makes its own decisions. If the spell indicates that the incarnate makes a decision, the GM determines what the incarnate would do. It might even become more inclined to do precisely as you wish over multiple summonings. The incarnate is not fully a creature. It can't take any other actions, nor can it be targeted or harmed by Strikes, spells, or other effects unless they would be able to target or end a spell effect (such as dispel magic). It has a size for the purposes of determining its placement for effects, but it doesn't block movement. If applicable, its effects use your spell DCs and spell attack roll modifier.", @@ -3898,7 +3865,7 @@ "TraitDescriptionLitany": "Litanies are special devotion spells, typically used by champions and requiring a single action, that usually give temporary immunity to further litanies.", "TraitDescriptionLizardfolk": "These reptilian humanoids, also known as iruxi, are extremely adaptable and patient.", "TraitDescriptionLozenge": "You Activate an alchemical lozenge by putting it in your mouth. It stays there, slowly dissolving and releasing its ingredients over time. You can bite a lozenge for a secondary effect. The action this takes is noted in the item. As soon as this secondary effect is over, the lozenge is used up and its benefits for you end. You can drink elixirs, potions, and beverages with a lozenge in your mouth, but you can't benefit from more than one lozenge at a time. If you have two lozenges in your mouth at the same time, both become inert. You can also spit out a lozenge as a single action to end its effect and make it inert. A lozenge dissolves due to its alchemical ingredients, so it typically still works even if you don't have saliva.", - "TraitDescriptionMagical": "Something with the magical trait is imbued with magical energies not tied to a specific tradition of magic. A magical item radiates a magic aura infused with its dominant school of magic. Some items or effects are closely tied to a particular tradition of magic. In these cases, the item has the arcane, divine, occult, or primal trait instead of the magical trait. Any of these traits indicate that the item is magical.", + "TraitDescriptionMagical": "Something with the magical trait is imbued with magical energies not tied to a specific tradition of magic. Some items or effects are closely tied to a particular tradition of magic. In these cases, the item has the arcane, divine, occult, or primal trait instead of the magical trait. Any of these traits indicate that the item is magical.", "TraitDescriptionMagus": "This indicates abilities from the magus class.", "TraitDescriptionManipulate": "You must physically manipulate an item or make gestures to use an action with this trait. Creatures without a suitable appendage can't perform actions with this trait. Manipulate actions often trigger reactions.", "TraitDescriptionMechanical": "A hazard with this trait is a constructed physical object.", @@ -3920,7 +3887,6 @@ "TraitDescriptionMulticlass": "Archetypes with the multiclass trait represent diversifying your training into another class's specialties. You can't select a multiclass archetype's dedication feat if you are a member of the class of the same name.", "TraitDescriptionMutagen": "An elixir with the mutagen trait temporarily transmogrifies the subject's body and alters its mind. A mutagen always conveys one or more beneficial effects paired with one or more detrimental effects. Mutagens are polymorph effects, meaning you can benefit from only one at a time.", "TraitDescriptionNagaji": "A traditionalist ancestry with reptilian features and serpentine heads.", - "TraitDescriptionNecromancy": "Effects and magic items with this trait are associated with the necromancy school of magic, typically involving forces of life and death.", "TraitDescriptionNephilim": "A creature with this trait has the nephilim versatile heritage. Nephilim are planar scions descended from immortal beings from other planes. An ability with this trait can be used or selected only by nephilim.", "TraitDescriptionNindoru": "Nindorus are chaotic evil fiends that rise from corruptions to the cycle of reincarnating souls and live on the Material Plane. Although they often have elements in their appearance that make them seem undead, they're actually living beings. Most nindorus have darkvision, are immune to death effects, have weakness to silver, and possess the power to manifest objects or creatures from their thoughts.", "TraitDescriptionNoisy": "This armor is loud and likely to alert others to your presence when you're using the Avoid Notice exploration activity. The armor's check penalty applies to Stealth checks even if you meet the required Strength score.", @@ -4022,7 +3988,6 @@ "TraitDescriptionThrown": "You can throw this weapon as a ranged attack. A thrown weapon adds your Strength modifier to damage just like a melee weapon does. When this trait appears on a melee weapon, it also includes the range increment.", "TraitDescriptionTiefling": "A creature with this trait is a member of the tiefling ancestry.", "TraitDescriptionTraining": "A training weapon is designed to be used when training an animal to participate in combat by identifying the target for the animal to attack. Striking a creature with a training weapon gives your animal companion or your bonded animal a +1 circumstance bonus to its next attack roll against that target.", - "TraitDescriptionTransmutation": "Effects and magic items with this trait are associated with the transmutation school of magic, typically changing something's form.", "TraitDescriptionTrap": "A hazard or item with this trait is constructed to hinder interlopers.", "TraitDescriptionTrip": "You can use this weapon to Trip with the Athletics skill even if you don't have a free hand. This uses the weapon's reach (if different from your own) and adds the weapon's item bonus to attack rolls as an item bonus to the Athletics check. If you critically fail a check to Trip using the weapon, you can drop the weapon to take the effects of a failure instead of a critical failure.", "TraitDescriptionTrueName": "Certain spells, feats, and items have the true name trait. This trait means they require you to know a creature's true name to use them.", @@ -4059,7 +4024,6 @@ "TraitDisarm": "Disarm", "TraitDisease": "Disease", "TraitDiv": "Div", - "TraitDivination": "Divination", "TraitDivine": "Divine", "TraitDoubleBarrel": "Double Barrel", "TraitDowntime": "Downtime", @@ -4079,7 +4043,6 @@ "TraitElf": "Elf", "TraitElixir": "Elixir", "TraitEmotion": "Emotion", - "TraitEnchantment": "Enchantment", "TraitEnergy": "Energy", "TraitEntrenchMelee": "Entrench Melee", "TraitEntrenchRanged": "Entrench Ranged", @@ -4108,7 +4071,6 @@ "TraitEtchedOntoMetalArmor": "Etched onto Metal Armor", "TraitEthereal": "Ethereal", "TraitEvil": "Evil", - "TraitEvocation": "Evocation", "TraitEvolution": "Evolution", "TraitExpandable": "Expandable", "TraitExploration": "Exploration", @@ -4133,6 +4095,7 @@ "TraitFleshwarp": "Fleshwarp", "TraitFlexible": "Flexible", "TraitFlourish": "Flourish", + "TraitFocus": "Focus", "TraitFocused": "Focused", "TraitFoldaway": "Foldaway", "TraitForce": "Force", @@ -4265,7 +4228,6 @@ "TraitMutagen": "Mutagen", "TraitMutant": "Mutant", "TraitNagaji": "Nagaji", - "TraitNecromancy": "Necromancy", "TraitNephilim": "Nephilim", "TraitNindoru": "Nindoru", "TraitNoisy": "Noisy", @@ -4510,7 +4472,6 @@ "TraitTitan": "Titan", "TraitTouched": "Touched", "TraitTraining": "Training", - "TraitTransmutation": "Transmutation", "TraitTrap": "Trap", "TraitTrip": "Trip", "TraitTroll": "Troll", diff --git a/static/template.json b/static/template.json index a8212e1a64e..8df87367a4e 100644 --- a/static/template.json +++ b/static/template.json @@ -1020,24 +1020,10 @@ "level": { "value": 1 }, - "spellType": { - "value": "attack" - }, - "category": { - "value": "spell" - }, - "traditions": { - "value": [] - }, - "components": { - "focus": false, - "material": false, - "somatic": false, - "verbal": false - }, - "materials": { - "value": "" + "traits": { + "traditions": [] }, + "requirements": "", "target": { "value": "" }, @@ -1049,27 +1035,18 @@ "value": "2" }, "duration": { - "value": "" - }, - "damage": { - "value": {} - }, - "save": { "value": "", - "basic": "" - }, - "sustained": { - "value": false + "sustained": false }, + "damage": {}, + "defense": null, "cost": { "value": "" }, "location": { "value": null }, - "hasCounteractCheck": { - "value": false - } + "counteraction": false }, "spellcastingEntry": { "templates": [ diff --git a/static/templates/items/spell-details.hbs b/static/templates/items/spell-details.hbs index c408d022bde..201663ccb45 100644 --- a/static/templates/items/spell-details.hbs +++ b/static/templates/items/spell-details.hbs @@ -1,105 +1,50 @@ -
-
  • - - -
  • - -
  • - - -
  • - - {{#unless isRitual}} -
  • - - -
  • +
    + {{#unless item.isRitual}} +
    + + +
    {{/unless}} -
    - -
    - {{localize "PF2E.CastLabel"}}
    - +
    - -
    - - - - -
    + +
    - - + +
    -
    - - -
    -
    - -{{#if isRitual}} -
      -

      {{localize "PF2E.SpellRitualLabel"}}

      - + {{#if item.isRitual}}
      - - + +
      - - + +
      - - + +
      -
    -{{/if}} - -
    - {{localize "PF2E.Usage"}} + {{/if}}
    - +
    - - -
    - -
    - +
    + +
    - - + +
    -
    - {{localize "PF2E.DamageLabel"}} +{{#unless item.isRitual}} +
    + {{localize "PF2E.Item.Spell.Defense.Label"}} -
    - - -
    + {{#if passiveDefense}} +
    + +
    {{localize passiveDefense}}
    +
    + {{/if}} -
    - -
    - - - {{#each saves as |ability a|}} - - {{/each}} - {{/select}} - + {{#select data.defense.save.statistic}} + + + + {{/select}} + +
    + + +
    +
    - +
    -
    -

    - {{localize "PF2E.FormulaPlaceholder"}} -
    - -
    -

    +
    + {{localize "PF2E.DamageLabel"}} - {{#each data.damage.value as |damage id|}} -
    -
    - - - - +
    +

    + {{localize "PF2E.FormulaPlaceholder"}} + {{#if editable}}
    - - -
    -

    - {{#if damage.type.categories}} -
    - {{#each damage.type.categories}} -
    {{localize (lookup @root.damageCategories this)}}
    - {{/each}} +
    {{/if}} -
    - {{/each}} -
    -
    + + + {{#each data.damage as |partial id|}} +
    +
    + + + + +
    + + +
    +
    + {{#if partial.materials}} +
    + {{#each partial.materials}} +
    {{localize (lookup @root.damageCategories this)}}
    + {{/each}} +
    + {{/if}} +
    + {{/each}} +
    +
    +{{/unless}} + +
    + Heightening -{{#unless data.heightening.type}} {{#if canHeighten}} - +
    + {{#if (not data.heightening)}} + + {{/if}} + {{#if (or (eq data.heightening.type "fixed") (not data.heightening))}} + + {{/if}} +
    {{/if}} -{{/unless}} -{{#if (eq data.heightening.type "interval")}} -
    + {{#if (eq data.heightening.type "interval")}}

    {{localize "PF2E.SpellScalingInterval.Header"}}

    -
    - -
    + {{#if editable}} +
    + +
    + {{/if}}
    - +
    - {{#each data.damage.value as |damage idx|}} + {{#each data.damage as |partial idx|}}
    - +
    {{/each}} -
    -{{/if}} - -{{! Spell heightening for fixed levels}} -{{#unless (eq data.heightening.type "interval")}} - {{#each heightenOverlays}} - {{> systems/pf2e/templates/items/spell-overlay.hbs this=this}} - {{/each}} - {{#if canHeighten}} - {{/if}} -{{/unless}} -{{#if (not isVariant)}} + {{!-- Spell heightening for fixed levels --}} + {{#unless (eq data.heightening.type "interval")}} + {{#each heightenOverlays}} + {{> systems/pf2e/templates/items/spell-overlay.hbs this=this}} + {{/each}} + {{/unless}} +
    + +{{#if (nor item.isRitual isVariant)}}
    {{localize "PF2E.Item.Spell.Variants.LabelPlural"}} @@ -278,11 +238,23 @@
    {{variant.actions}} -
    - - -
    + {{#if @root.editable}} +
    + + +
    + {{/if}}
    {{/each}}
    + +
    + + +
    {{/if}} + +
    + + +
    diff --git a/static/templates/items/spell-overlay.hbs b/static/templates/items/spell-overlay.hbs index 89989046677..17e1728e00f 100644 --- a/static/templates/items/spell-overlay.hbs +++ b/static/templates/items/spell-overlay.hbs @@ -1,7 +1,7 @@ -
    +
    {{#if (eq type "heighten")}}

    - +
    - +

    @@ -21,7 +21,7 @@
    - +
    @@ -29,7 +29,7 @@
    {{#each missing}} - @@ -38,7 +38,7 @@ {{#if (includes system "traits")}}
    - +
    @@ -63,7 +63,7 @@ {{#if (includes system "components")}}
    @@ -90,7 +90,7 @@ {{#if (includes system "target")}}
    @@ -102,7 +102,7 @@ {{#if (includes system "area")}}
    @@ -127,7 +127,7 @@ {{#if (includes system "range")}}
    @@ -139,7 +139,7 @@ {{#if (includes system "damage")}}

    - + {{localize "PF2E.FormulaPlaceholder"}}
    @@ -191,4 +191,4 @@ {{/each}}
    {{/if}} -

    + diff --git a/static/templates/items/spell-sidebar.hbs b/static/templates/items/spell-sidebar.hbs index a44538b114b..8dba0d06357 100644 --- a/static/templates/items/spell-sidebar.hbs +++ b/static/templates/items/spell-sidebar.hbs @@ -1,7 +1,3 @@ -{{#if document.school}} - {{localize (lookup magicSchools document.school)}} -{{/if}} -{{localize (lookup spellTypes data.spellType.value)}} {{{actionGlyph data.time.value fallback=true}}}
      @@ -9,18 +5,3 @@
    1. {{this}}
    2. {{/each}}
    -{{#if (or data.prepared.value data.concentration.value data.ritual.value)}} -
      - {{#if data.prepared.value}} -
    1. {{localize "PF2E.SpellPreparedLabel"}}
    2. - {{/if}} - - {{#if data.concentration.value}} -
    3. {{localize "PF2E.SpellConcentrationLabel"}}
    4. - {{/if}} - - {{#if data.ritual.value}} -
    5. {{localize "PF2E.SpellRitualLabel"}}
    6. - {{/if}} -
    -{{/if}}