From 5e899554c3756fb9b45cbc6b30fac29387cc3833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sim=C3=B5es?= Date: Thu, 14 Dec 2023 15:09:04 -0600 Subject: [PATCH] Build out basic structure for doubly-embedded items --- build/lib/extractor.ts | 4 ++++ build/run-migration.ts | 20 +++++++++++++++- src/module/item/armor/data.ts | 7 +++++- src/module/item/base/document.ts | 7 +++--- src/module/item/book/data.ts | 29 +++++++++++----------- src/module/item/equipment/data.ts | 10 ++++---- src/module/item/physical/document.ts | 36 ++++++++++++++++++++++++---- src/module/item/shield/data.ts | 7 +++++- src/module/item/spell/document.ts | 8 +++---- src/module/item/weapon/data.ts | 8 ++++++- src/module/item/weapon/document.ts | 3 ++- src/module/migration/runner/base.ts | 18 +++++++++++++- src/module/migration/runner/index.ts | 4 ++-- static/template.json | 14 +++++++---- tests/mocks/actor.ts | 2 ++ tests/module/migration.test.ts | 7 +++++- 16 files changed, 141 insertions(+), 43 deletions(-) diff --git a/build/lib/extractor.ts b/build/lib/extractor.ts index 4dcff4bb1c3..28ee1ef4249 100644 --- a/build/lib/extractor.ts +++ b/build/lib/extractor.ts @@ -522,6 +522,10 @@ class PackExtractor { delete (source.system as { specific?: unknown }).specific; } + if ("subitems" in source.system && source.system.subitems.length === 0) { + delete (source.system as { subitems?: unknown[] }).subitems; + } + if (source.type === "weapon") { delete (source.system as { property1?: unknown }).property1; if ("value" in source.system.damage) { diff --git a/build/run-migration.ts b/build/run-migration.ts index 6293c928ab0..7436d573fec 100644 --- a/build/run-migration.ts +++ b/build/run-migration.ts @@ -1,6 +1,7 @@ import { ActorSourcePF2e } from "@actor/data/index.ts"; import { CREATURE_ACTOR_TYPES } from "@actor/values.ts"; -import { ItemSourcePF2e } from "@item/base/data/index.ts"; +import { ItemSourcePF2e, isPhysicalData } from "@item/base/data/index.ts"; +import { itemIsOfType } from "@item/helpers.ts"; import { PHYSICAL_ITEM_TYPES } from "@item/physical/values.ts"; import { MigrationBase } from "@module/migration/base.ts"; import { MigrationRunnerBase } from "@module/migration/runner/base.ts"; @@ -168,6 +169,9 @@ async function migrate() { if (isActorData(source)) { for (const embedded of source.items) { embedded.flags ??= {}; + if (itemIsOfType(embedded, "armor", "equipment", "shield", "weapon")) { + embedded.system.subitems ??= []; + } } const update = await migrationRunner.getUpdatedActor(source, migrationRunner.migrations); @@ -184,12 +188,22 @@ async function migrate() { if (updatedItem.type === "consumable" && !updatedItem.system.spell) { delete (updatedItem.system as { spell?: object }).spell; } + if ( + isPhysicalData(updatedItem) && + "subitems" in updatedItem.system && + updatedItem.system.subitems.length === 0 + ) { + delete (updatedItem.system as { subitems?: unknown[] }).subitems; + } pruneFlags(updatedItem); } return fu.mergeObject(source, update, { inplace: false, performDeletions: true }); } else if (isItemData(source)) { source.system.slug = sluggify(source.name); + if (itemIsOfType(source, "armor", "equipment", "shield", "weapon")) { + source.system.subitems ??= []; + } const update = await migrationRunner.getUpdatedItem(source, migrationRunner.migrations); delete (source.system as { slug?: string }).slug; @@ -198,6 +212,10 @@ async function migrate() { if (update.type === "consumable" && !update.system.spell) { delete (update.system as { spell?: null }).spell; } + if (isPhysicalData(source) && "subitems" in source.system && source.system.subitems.length === 0) { + delete (source.system as { subitems?: unknown[] }).subitems; + } + pruneFlags(source); pruneFlags(update); diff --git a/src/module/item/armor/data.ts b/src/module/item/armor/data.ts index 04992e6d517..c9e1eea5d53 100644 --- a/src/module/item/armor/data.ts +++ b/src/module/item/armor/data.ts @@ -1,3 +1,4 @@ +import { PhysicalItemSource } from "@item/base/data/index.ts"; import { BasePhysicalItemSource, Investable, @@ -25,6 +26,8 @@ interface ArmorSystemSource extends Investable { runes: ArmorRuneSource; /** Details of specific magic armor, storing the material and rune state when toggled on */ specific: SpecificArmorData | null; + /** Doubly-embedded adjustments, attachments, talismans etc. */ + subitems: PhysicalItemSource[]; /** Usage for armor isn't stored. */ readonly usage?: never; } @@ -42,7 +45,7 @@ type SpecificArmorData = { }; interface ArmorSystemData - extends Omit, + extends Omit, Omit, "baseItem" | "traits"> { runes: ArmorRuneData; /** Armor is always worn in the "armor" slot. */ @@ -50,6 +53,8 @@ interface ArmorSystemData stackGroup: null; } +type SourceOmission = "bulk" | "hp" | "identification" | "items" | "material" | "price" | "temporary" | "usage"; + interface ArmorTraits extends PhysicalItemTraits { otherTags: OtherArmorTag[]; } diff --git a/src/module/item/base/document.ts b/src/module/item/base/document.ts index a6aab6933dc..0a599e30ef9 100644 --- a/src/module/item/base/document.ts +++ b/src/module/item/base/document.ts @@ -862,11 +862,12 @@ const ItemProxyPF2e = new Proxy(ItemPF2e, { _target, args: [source: PreCreate, context?: DocumentConstructionContext], ) { + const source = args[0]; const type = - args[0]?.type === "armor" && (args[0].system?.category as string | undefined) === "shield" + source?.type === "armor" && (source.system?.category as string | undefined) === "shield" ? "shield" - : args[0]?.type; - const ItemClass = CONFIG.PF2E.Item.documentClasses[type] ?? ItemPF2e; + : source?.type; + const ItemClass: typeof ItemPF2e = CONFIG.PF2E.Item.documentClasses[type] ?? ItemPF2e; return new ItemClass(...args); }, }); diff --git a/src/module/item/book/data.ts b/src/module/item/book/data.ts index 8a576a2f7b8..35a552f137b 100644 --- a/src/module/item/book/data.ts +++ b/src/module/item/book/data.ts @@ -1,22 +1,23 @@ -import { EquipmentSystemData, EquipmentSystemSource } from "@item/equipment/data.ts"; -import { BasePhysicalItemSource } from "@item/physical/data.ts"; +import { EquipmentTrait } from "@item/equipment/data.ts"; +import { + BasePhysicalItemSource, + PhysicalItemTraits, + PhysicalSystemData, + PhysicalSystemSource, +} from "@item/physical/data.ts"; type BookSource = BasePhysicalItemSource<"book", BookSystemSource>; +type BookTraits = PhysicalItemTraits; -type BookSystemSource = EquipmentSystemSource & { +interface BookSystemSource extends PhysicalSystemSource { + traits: BookTraits; + category: "formula" | "spell"; capacity: number; -} & (FormulaBookData | SpellBookData); - -type BookSystemData = Omit & EquipmentSystemData; - -interface FormulaBookData { - subtype: "formula"; - item: ItemUUID[]; + contents: ItemUUID[]; } -interface SpellBookData { - subtype: "spell"; - item: object[]; -} +interface BookSystemData extends Omit, PhysicalSystemData {} + +type SourceOmission = "bulk" | "hp" | "identification" | "material" | "price" | "temporary" | "traits" | "usage"; export type { BookSource, BookSystemData }; diff --git a/src/module/item/equipment/data.ts b/src/module/item/equipment/data.ts index 39832bc4125..f6fafa5de7b 100644 --- a/src/module/item/equipment/data.ts +++ b/src/module/item/equipment/data.ts @@ -1,4 +1,5 @@ import { AttributeString } from "@actor/types.ts"; +import { PhysicalItemSource } from "@item/base/data/index.ts"; import { BasePhysicalItemSource, Investable, @@ -23,13 +24,12 @@ interface EquipmentSystemSource extends Investable { }; usage: { value: string }; + /** Doubly-embedded adjustments, attachments, talismans etc. */ + subitems: PhysicalItemSource[]; } interface EquipmentSystemData - extends Omit< - EquipmentSystemSource, - "bulk" | "hp" | "identification" | "material" | "price" | "temporary" | "usage" - >, + extends Omit, Omit, "traits"> { apex?: { attribute: AttributeString; @@ -38,6 +38,8 @@ interface EquipmentSystemData stackGroup: null; } +type SourceOmission = "bulk" | "hp" | "identification" | "items" | "material" | "price" | "temporary" | "usage"; + interface EquipmentTraits extends PhysicalItemTraits { otherTags: OtherEquipmentTag[]; } diff --git a/src/module/item/physical/document.ts b/src/module/item/physical/document.ts index b2ee2867d5c..aa613fd9f53 100644 --- a/src/module/item/physical/document.ts +++ b/src/module/item/physical/document.ts @@ -1,6 +1,6 @@ import { type ActorPF2e } from "@actor"; -import { ItemPF2e, type ContainerPF2e } from "@item"; -import { ItemSummaryData, PhysicalItemSource, TraitChatData } from "@item/base/data/index.ts"; +import { ItemPF2e, ItemProxyPF2e, type ContainerPF2e } from "@item"; +import { ItemSourcePF2e, ItemSummaryData, PhysicalItemSource, TraitChatData } from "@item/base/data/index.ts"; import { MystifiedTraits } from "@item/base/data/values.ts"; import { isCycle } from "@item/container/helpers.ts"; import { Rarity, Size, ZeroToTwo } from "@module/data.ts"; @@ -25,9 +25,23 @@ import { getUsageDetails, isEquipped } from "./usage.ts"; import { DENOMINATIONS } from "./values.ts"; abstract class PhysicalItemPF2e extends ItemPF2e { - // The cached container of this item, if in a container, or null + /** The item in which this item is embedded */ + parentItem: PhysicalItemPF2e | null; + + /** + * The cached container of this item, if in a container, or null + * @ignore + */ private declare _container: ContainerPF2e | null; + /** Doubly-embedded adjustments, attachments, talismans etc. */ + declare subitems: PhysicalItemPF2e[]; + + constructor(data: PreCreate, context: PhysicalItemConstructionContext = {}) { + super(data, context); + this.parentItem = context.parentItem ?? null; + } + get level(): number { return this.system.level.value; } @@ -258,6 +272,15 @@ abstract class PhysicalItemPF2e + new ItemProxyPF2e(i, { parent: this.parent, parentItem: this }) as PhysicalItemPF2e, + ) + : []; } /** Refresh certain derived properties in case of special data preparation from subclasses */ @@ -583,4 +606,9 @@ interface PhysicalItemPF2e system: PhysicalSystemData; } -export { PhysicalItemPF2e }; +interface PhysicalItemConstructionContext + extends DocumentConstructionContext { + parentItem?: PhysicalItemPF2e; +} + +export { PhysicalItemPF2e, type PhysicalItemConstructionContext }; diff --git a/src/module/item/shield/data.ts b/src/module/item/shield/data.ts index e9aeb9689ee..02fcd2cc2ff 100644 --- a/src/module/item/shield/data.ts +++ b/src/module/item/shield/data.ts @@ -1,3 +1,4 @@ +import { PhysicalItemSource } from "@item/base/data/index.ts"; import { BasePhysicalItemSource, PhysicalItemTraits, @@ -23,6 +24,8 @@ interface ShieldSystemSource extends PhysicalSystemSource { runes: ShieldRuneData; /** Usage for shields isn't stored. */ readonly usage?: never; + /** Doubly-embedded adjustments, attachments, talismans etc. */ + subitems: PhysicalItemSource[]; } interface IntegratedWeaponSource { @@ -42,7 +45,7 @@ interface SpecificShieldData extends Pick, + extends Omit, Omit { traits: ShieldTraits; /** Shields are always held. */ @@ -50,6 +53,8 @@ interface ShieldSystemData stackGroup: null; } +type SourceOmission = "bulk" | "hp" | "identification" | "items" | "material" | "price" | "temporary" | "usage"; + interface IntegratedWeaponData extends IntegratedWeaponSource { damageType: DamageType; versatile: { options: DamageType[]; selection: DamageType } | null; diff --git a/src/module/item/spell/document.ts b/src/module/item/spell/document.ts index 0dd431980b2..43cb7171276 100644 --- a/src/module/item/spell/document.ts +++ b/src/module/item/spell/document.ts @@ -59,10 +59,6 @@ import { createDescriptionPrepend, createSpellRankLabel } from "./helpers.ts"; import { SpellOverlayCollection } from "./overlay.ts"; import { EffectAreaSize, MagicTradition, SpellTrait } from "./types.ts"; -interface SpellConstructionContext extends DocumentConstructionContext { - fromConsumable?: boolean; -} - class SpellPF2e extends ItemPF2e { readonly isFromConsumable: boolean; @@ -1089,6 +1085,10 @@ interface SpellPF2e extends system: SpellSystemData; } +interface SpellConstructionContext extends DocumentConstructionContext { + fromConsumable?: boolean; +} + interface SpellDamage { template: SpellDamageTemplate; context: DamageRollContext; diff --git a/src/module/item/weapon/data.ts b/src/module/item/weapon/data.ts index 8ae25fdcfe9..71a0cfcbdde 100644 --- a/src/module/item/weapon/data.ts +++ b/src/module/item/weapon/data.ts @@ -1,4 +1,5 @@ import { AttributeString } from "@actor/types.ts"; +import { PhysicalItemSource } from "@item/base/data/index.ts"; import { ItemFlagsPF2e } from "@item/base/data/system.ts"; import { BasePhysicalItemSource, @@ -82,6 +83,9 @@ interface WeaponSystemSource extends Investable { /** Whether this is an unarmed attack that is a grasping appendage, requiring a free hand for use */ graspingAppendage?: boolean; + /** Doubly-embedded adjustments, attachments, talismans etc. */ + subitems: PhysicalItemSource[]; + // Refers to custom damage, *not* property runes property1: { value: string; @@ -139,7 +143,7 @@ type WeaponRuneSource = { }; interface WeaponSystemData - extends Omit, + extends Omit, Omit, "material"> { traits: WeaponTraits; baseItem: BaseWeaponType | null; @@ -159,6 +163,8 @@ interface WeaponSystemData stackGroup: null; } +type SourceOmission = "bulk" | "hp" | "identification" | "items" | "price" | "temporary"; + type WeaponUsageDetails = UsageDetails & Required; interface WeaponTraits extends WeaponTraitsSource { diff --git a/src/module/item/weapon/document.ts b/src/module/item/weapon/document.ts index 77a152f32c1..f6e2aace700 100644 --- a/src/module/item/weapon/document.ts +++ b/src/module/item/weapon/document.ts @@ -7,6 +7,7 @@ import { ConsumablePF2e, MeleePF2e, PhysicalItemPF2e, ShieldPF2e } from "@item"; import { createActionRangeLabel } from "@item/ability/helpers.ts"; import { ItemSourcePF2e, ItemSummaryData, MeleeSource } from "@item/base/data/index.ts"; import { NPCAttackDamage, NPCAttackTrait } from "@item/melee/data.ts"; +import { PhysicalItemConstructionContext } from "@item/physical/document.ts"; import { IdentificationStatus, MystifiedData, RUNE_DATA, getPropertyRuneSlots } from "@item/physical/index.ts"; import { MAGIC_TRADITIONS } from "@item/spell/values.ts"; import { RangeData } from "@item/types.ts"; @@ -728,7 +729,7 @@ interface WeaponPF2e extend get traits(): Set; } -interface WeaponConstructionContext extends DocumentConstructionContext { +interface WeaponConstructionContext extends PhysicalItemConstructionContext { shield?: ShieldPF2e; } diff --git a/src/module/migration/runner/base.ts b/src/module/migration/runner/base.ts index d44ccc0b391..4fceae61008 100644 --- a/src/module/migration/runner/base.ts +++ b/src/module/migration/runner/base.ts @@ -1,5 +1,6 @@ import { ActorSourcePF2e } from "@actor/data/index.ts"; import { ItemSourcePF2e } from "@item/base/data/index.ts"; +import { itemIsOfType } from "@item/helpers.ts"; import { MigrationRecord } from "@module/data.ts"; import { MigrationBase } from "@module/migration/base.ts"; import type { ScenePF2e, TokenDocumentPF2e } from "@scene"; @@ -78,6 +79,11 @@ export class MigrationRunnerBase { if (currentItem.type === "consumable" && currentItem.system.spell) { await migration.preUpdateItem?.(currentItem.system.spell); } + if (itemIsOfType(currentItem, "armor", "equipment", "shield", "weapon")) { + for (const embed of currentItem.system.subitems) { + migration.preUpdateItem?.(embed); + } + } } } @@ -86,10 +92,15 @@ export class MigrationRunnerBase { for (const currentItem of currentActor.items) { await migration.updateItem?.(currentItem, currentActor); - // Handle embedded spells + // Handle embedded items if (currentItem.type === "consumable" && currentItem.system.spell) { await migration.updateItem?.(currentItem.system.spell, currentActor); } + if (itemIsOfType(currentItem, "armor", "equipment", "shield", "weapon")) { + for (const embed of currentItem.system.subitems) { + migration.preUpdateItem?.(embed); + } + } } } @@ -115,6 +126,11 @@ export class MigrationRunnerBase { if (current.type === "consumable" && current.system.spell) { await migration.preUpdateItem?.(current.system.spell); } + if (itemIsOfType(current, "armor", "equipment", "shield", "weapon")) { + for (const embed of current.system.subitems) { + migration.preUpdateItem?.(embed); + } + } } for (const migration of migrations) { diff --git a/src/module/migration/runner/index.ts b/src/module/migration/runner/index.ts index 6f5e2f84dc2..b6542363e79 100644 --- a/src/module/migration/runner/index.ts +++ b/src/module/migration/runner/index.ts @@ -37,7 +37,7 @@ export class MigrationRunner extends MigrationRunnerBase { document.updateSource({ "system._migration.version": currentVersion }); // Discriminate between item and actor without importing, which would throw errors on the migration test - if ("items" in document && "token" in document) { + if ("items" in document && "prototypeToken" in document) { for (const item of document.items) { if (!item.schemaVersion) { item.updateSource({ "system._migration.version": currentVersion }); @@ -68,7 +68,7 @@ export class MigrationRunner extends MigrationRunnerBase { } } const updated = - "items" in document + "prototypeToken" in document ? await this.#migrateActor(migrations, document, { pack }) : await this.#migrateItem(migrations, document); if (updated) updateGroup.push(updated); diff --git a/static/template.json b/static/template.json index dd82268a75c..673dbc335ae 100644 --- a/static/template.json +++ b/static/template.json @@ -648,9 +648,9 @@ "common", "physical" ], - "subtype": "formula", + "category": "formula", "capacity": 1, - "items": [] + "contents": [] }, "treasure": { "templates": [ @@ -698,6 +698,7 @@ "property": [] }, "specific": null, + "subitems": [], "property1": { "value": "", "dice": 0, @@ -726,7 +727,8 @@ "potency": 0, "resilient": 0, "property": [] - } + }, + "subitems": [] }, "melee": { "templates": [ @@ -778,7 +780,8 @@ ], "usage": { "value": "held-in-one-hand" - } + }, + "subitems": [] }, "ancestry": { "templates": [ @@ -1032,7 +1035,8 @@ "speedPenalty": 0, "traits": { "integrated": null - } + }, + "subitems": [] }, "spell": { "templates": [ diff --git a/tests/mocks/actor.ts b/tests/mocks/actor.ts index 8afc6dc54e4..2d29cba3a02 100644 --- a/tests/mocks/actor.ts +++ b/tests/mocks/actor.ts @@ -18,6 +18,8 @@ export class MockActor { readonly effects: MockCollection> = new MockCollection(); + prototypeToken = {}; + _itemGuid = 1; constructor( diff --git a/tests/module/migration.test.ts b/tests/module/migration.test.ts index dfaff1078c4..c31ba65316f 100644 --- a/tests/module/migration.test.ts +++ b/tests/module/migration.test.ts @@ -2,7 +2,8 @@ // @ts-nocheck import { ActorSourcePF2e, CharacterSource } from "@actor/data/index.ts"; -import { ArmorSource, ItemSourcePF2e } from "@item/data/index.ts"; +import { ArmorSource, ItemSourcePF2e } from "@item/base/data/index.ts"; +import { itemIsOfType } from "@item/helpers.ts"; import { MigrationBase } from "@module/migration/base.ts"; import { MigrationRunner } from "@module/migration/runner/index.ts"; import { MockActor } from "tests/mocks/actor.ts"; @@ -22,11 +23,15 @@ characterData.effects = []; characterData.system._migration = { version: 0, previous: null }; for (const item of characterData.items) { item.effects = []; + if (itemIsOfType(item, "armor", "equipment", "shield", "weapon")) { + item.system.subitems ??= []; + } item.system._migration = { version: 0, previous: null }; } const armorData = fu.duplicate(armorJSON) as unknown as ArmorSource; armorData.effects = []; +armorData.system.subitems ??= []; armorData.system._migration = { version: 0, previous: null }; describe("test migration runner", () => {