Skip to content

Commit

Permalink
Build out basic structure for doubly-embedded items (foundryvtt#12145)
Browse files Browse the repository at this point in the history
  • Loading branch information
stwlam committed Dec 19, 2023
1 parent ca7d6f2 commit cb58e85
Show file tree
Hide file tree
Showing 16 changed files with 141 additions and 43 deletions.
4 changes: 4 additions & 0 deletions build/lib/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
20 changes: 19 additions & 1 deletion build/run-migration.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -170,6 +171,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);
Expand All @@ -186,12 +190,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;
Expand All @@ -200,6 +214,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);

Expand Down
7 changes: 6 additions & 1 deletion src/module/item/armor/data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PhysicalItemSource } from "@item/base/data/index.ts";
import {
BasePhysicalItemSource,
Investable,
Expand Down Expand Up @@ -25,6 +26,8 @@ interface ArmorSystemSource extends Investable<PhysicalSystemSource> {
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;
}
Expand All @@ -42,14 +45,16 @@ type SpecificArmorData = {
};

interface ArmorSystemData
extends Omit<ArmorSystemSource, "bulk" | "hp" | "identification" | "material" | "price" | "temporary" | "usage">,
extends Omit<ArmorSystemSource, SourceOmission>,
Omit<Investable<PhysicalSystemData>, "baseItem" | "traits"> {
runes: ArmorRuneData;
/** Armor is always worn in the "armor" slot. */
usage: WornUsage;
stackGroup: null;
}

type SourceOmission = "bulk" | "hp" | "identification" | "items" | "material" | "price" | "temporary" | "usage";

interface ArmorTraits extends PhysicalItemTraits<ArmorTrait> {
otherTags: OtherArmorTag[];
}
Expand Down
7 changes: 4 additions & 3 deletions src/module/item/base/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,11 +862,12 @@ const ItemProxyPF2e = new Proxy(ItemPF2e, {
_target,
args: [source: PreCreate<ItemSourcePF2e>, context?: DocumentConstructionContext<ActorPF2e | null>],
) {
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);
},
});
Expand Down
29 changes: 15 additions & 14 deletions src/module/item/book/data.ts
Original file line number Diff line number Diff line change
@@ -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<EquipmentTrait>;

type BookSystemSource = EquipmentSystemSource & {
interface BookSystemSource extends PhysicalSystemSource {
traits: BookTraits;
category: "formula" | "spell";
capacity: number;
} & (FormulaBookData | SpellBookData);

type BookSystemData = Omit<BookSystemSource, "hp" | "price"> & EquipmentSystemData;

interface FormulaBookData {
subtype: "formula";
item: ItemUUID[];
contents: ItemUUID[];
}

interface SpellBookData {
subtype: "spell";
item: object[];
}
interface BookSystemData extends Omit<BookSystemSource, SourceOmission>, PhysicalSystemData {}

type SourceOmission = "bulk" | "hp" | "identification" | "material" | "price" | "temporary" | "traits" | "usage";

export type { BookSource, BookSystemData };
10 changes: 6 additions & 4 deletions src/module/item/equipment/data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AttributeString } from "@actor/types.ts";
import { PhysicalItemSource } from "@item/base/data/index.ts";
import {
BasePhysicalItemSource,
Investable,
Expand All @@ -23,13 +24,12 @@ interface EquipmentSystemSource extends Investable<PhysicalSystemSource> {
};

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<EquipmentSystemSource, SourceOmission>,
Omit<Investable<PhysicalSystemData>, "traits"> {
apex?: {
attribute: AttributeString;
Expand All @@ -38,6 +38,8 @@ interface EquipmentSystemData
stackGroup: null;
}

type SourceOmission = "bulk" | "hp" | "identification" | "items" | "material" | "price" | "temporary" | "usage";

interface EquipmentTraits extends PhysicalItemTraits<EquipmentTrait> {
otherTags: OtherEquipmentTag[];
}
Expand Down
36 changes: 32 additions & 4 deletions src/module/item/physical/document.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,9 +25,23 @@ import { getUsageDetails, isEquipped } from "./usage.ts";
import { DENOMINATIONS } from "./values.ts";

abstract class PhysicalItemPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends ItemPF2e<TParent> {
// 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<ActorPF2e> | null;

/** Doubly-embedded adjustments, attachments, talismans etc. */
declare subitems: PhysicalItemPF2e<TParent>[];

constructor(data: PreCreate<ItemSourcePF2e>, context: PhysicalItemConstructionContext<TParent> = {}) {
super(data, context);
this.parentItem = context.parentItem ?? null;
}

get level(): number {
return this.system.level.value;
}
Expand Down Expand Up @@ -258,6 +272,15 @@ abstract class PhysicalItemPF2e<TParent extends ActorPF2e | null = ActorPF2e | n
if (this._container?.id !== this.system.containerId) {
this._container = null;
}

// Prepare doubly-embedded items if this is of an appropriate physical-item type
this.subitems =
"subitems" in this.system && Array.isArray(this.system.subitems)
? this.system.subitems.map(
(i) =>
new ItemProxyPF2e(i, { parent: this.parent, parentItem: this }) as PhysicalItemPF2e<TParent>,
)
: [];
}

/** Refresh certain derived properties in case of special data preparation from subclasses */
Expand Down Expand Up @@ -583,4 +606,9 @@ interface PhysicalItemPF2e<TParent extends ActorPF2e | null = ActorPF2e | null>
system: PhysicalSystemData;
}

export { PhysicalItemPF2e };
interface PhysicalItemConstructionContext<TParent extends ActorPF2e | null>
extends DocumentConstructionContext<TParent> {
parentItem?: PhysicalItemPF2e<TParent>;
}

export { PhysicalItemPF2e, type PhysicalItemConstructionContext };
7 changes: 6 additions & 1 deletion src/module/item/shield/data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PhysicalItemSource } from "@item/base/data/index.ts";
import {
BasePhysicalItemSource,
PhysicalItemTraits,
Expand All @@ -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 {
Expand All @@ -42,14 +45,16 @@ interface SpecificShieldData extends Pick<ShieldSystemSource, "material" | "rune
}

interface ShieldSystemData
extends Omit<ShieldSystemSource, "bulk" | "hp" | "identification" | "material" | "price" | "temporary" | "usage">,
extends Omit<ShieldSystemSource, SourceOmission>,
Omit<PhysicalSystemData, "baseItem" | "traits"> {
traits: ShieldTraits;
/** Shields are always held. */
usage: HeldUsage;
stackGroup: null;
}

type SourceOmission = "bulk" | "hp" | "identification" | "items" | "material" | "price" | "temporary" | "usage";

interface IntegratedWeaponData extends IntegratedWeaponSource {
damageType: DamageType;
versatile: { options: DamageType[]; selection: DamageType } | null;
Expand Down
8 changes: 4 additions & 4 deletions src/module/item/spell/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ import { createDescriptionPrepend, createSpellRankLabel } from "./helpers.ts";
import { SpellOverlayCollection } from "./overlay.ts";
import { EffectAreaSize, MagicTradition, SpellTrait } from "./types.ts";

interface SpellConstructionContext<TParent extends ActorPF2e | null> extends DocumentConstructionContext<TParent> {
fromConsumable?: boolean;
}

class SpellPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends ItemPF2e<TParent> {
readonly isFromConsumable: boolean;

Expand Down Expand Up @@ -1099,6 +1095,10 @@ interface SpellPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends
system: SpellSystemData;
}

interface SpellConstructionContext<TParent extends ActorPF2e | null> extends DocumentConstructionContext<TParent> {
fromConsumable?: boolean;
}

interface SpellDamage {
template: SpellDamageTemplate;
context: DamageRollContext;
Expand Down
8 changes: 7 additions & 1 deletion src/module/item/weapon/data.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -82,6 +83,9 @@ interface WeaponSystemSource extends Investable<PhysicalSystemSource> {
/** 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;
Expand Down Expand Up @@ -139,7 +143,7 @@ type WeaponRuneSource = {
};

interface WeaponSystemData
extends Omit<WeaponSystemSource, "bulk" | "hp" | "identification" | "price" | "temporary">,
extends Omit<WeaponSystemSource, SourceOmission>,
Omit<Investable<PhysicalSystemData>, "material"> {
traits: WeaponTraits;
baseItem: BaseWeaponType | null;
Expand All @@ -159,6 +163,8 @@ interface WeaponSystemData
stackGroup: null;
}

type SourceOmission = "bulk" | "hp" | "identification" | "items" | "price" | "temporary";

type WeaponUsageDetails = UsageDetails & Required<WeaponSystemSource["usage"]>;

interface WeaponTraits extends WeaponTraitsSource {
Expand Down
3 changes: 2 additions & 1 deletion src/module/item/weapon/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { 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";
Expand Down Expand Up @@ -728,7 +729,7 @@ interface WeaponPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extend
get traits(): Set<WeaponTrait>;
}

interface WeaponConstructionContext<TParent extends ActorPF2e | null> extends DocumentConstructionContext<TParent> {
interface WeaponConstructionContext<TParent extends ActorPF2e | null> extends PhysicalItemConstructionContext<TParent> {
shield?: ShieldPF2e<TParent>;
}

Expand Down
18 changes: 17 additions & 1 deletion src/module/migration/runner/base.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}
}
}
}

Expand All @@ -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);
}
}
}
}

Expand All @@ -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) {
Expand Down
Loading

0 comments on commit cb58e85

Please sign in to comment.