diff --git a/build/lib/compendium-pack.ts b/build/lib/compendium-pack.ts index 35add8597fe..84407c8e656 100644 --- a/build/lib/compendium-pack.ts +++ b/build/lib/compendium-pack.ts @@ -333,7 +333,7 @@ class CompendiumPack { if (itemIsOfType(source, "feat", "action") && source.system.selfEffect) { source.system.selfEffect.uuid = CompendiumPack.convertUUID(source.system.selfEffect.uuid, convertOptions); } else if (itemIsOfType(source, "ancestry", "background", "class", "kit")) { - const items: Record }> = + const items: Record | null }> = source.system.items; for (const entry of Object.values(items)) { entry.uuid = CompendiumPack.convertUUID(entry.uuid, convertOptions); diff --git a/src/module/data.ts b/src/module/data.ts index 2e3505207d9..a6b468ac5db 100644 --- a/src/module/data.ts +++ b/src/module/data.ts @@ -73,11 +73,11 @@ interface NewDocumentMigrationRecord { } interface MigratedDocumentMigrationRecord { - version: number; + version: number | null; previous: { schema: number | null; - system?: string; - foundry?: string; + system: string | null; + foundry: string | null; } | null; } diff --git a/src/module/item/base/data/schema.ts b/src/module/item/base/data/schema.ts new file mode 100644 index 00000000000..2f74f304fdb --- /dev/null +++ b/src/module/item/base/data/schema.ts @@ -0,0 +1,118 @@ +import { RuleElementSource } from "@module/rules/index.ts"; +import { SlugField } from "@system/schema-data-fields.ts"; +import type { + ArrayField, + BooleanField, + NumberField, + ObjectField, + SchemaField, + StringField, +} from "types/foundry/common/data/fields.d.ts"; +import type { ItemPF2e } from "../document.ts"; +import { ItemDescriptionData } from "./system.ts"; + +abstract class ItemSystemModel extends foundry.abstract + .TypeDataModel { + static override defineSchema(): ItemSystemSchema { + const fields = foundry.data.fields; + + const anyStringField = (): StringField => + new fields.StringField({ required: true, nullable: false, initial: "" }); + + return { + description: new fields.SchemaField({ + value: anyStringField(), + gm: anyStringField(), + }), + publication: new fields.SchemaField({ + title: anyStringField(), + authors: anyStringField(), + license: new fields.StringField({ + required: true, + nullable: false, + choices: ["OGL", "ORC"], + initial: "OGL", + }), + remaster: new fields.BooleanField({ required: true, nullable: false, initial: false }), + }), + rules: new fields.ArrayField(new fields.ObjectField({ required: true, nullable: false }), { + required: true, + nullable: false, + }), + slug: new SlugField({ required: true, nullable: true, initial: null }), + traits: new fields.SchemaField({ + otherTags: new fields.ArrayField( + new SlugField({ required: true, nullable: false, initial: undefined }), + ), + }), + _migration: new fields.SchemaField({ + version: new fields.NumberField({ + required: true, + nullable: true, + positive: true, + initial: null, + }), + previous: new fields.SchemaField( + { + foundry: new fields.StringField({ required: true, nullable: true, initial: null }), + system: new fields.StringField({ required: true, nullable: true, initial: null }), + schema: new fields.NumberField({ + required: true, + nullable: true, + positive: true, + initial: null, + }), + }, + { required: true, nullable: true, initial: null }, + ), + }), + }; + } +} + +interface ItemSystemModel + extends foundry.abstract.TypeDataModel { + description: ItemDescriptionData; +} + +type ItemSystemSchema = { + description: SchemaField<{ + value: StringField; + gm: StringField; + }>; + publication: SchemaField<{ + title: StringField; + authors: StringField; + license: StringField<"OGL" | "ORC", "OGL" | "ORC", true, false, true>; + remaster: BooleanField; + }>; + rules: ArrayField< + ObjectField, + RuleElementSource[], + RuleElementSource[], + true, + false, + true + >; + slug: SlugField; + traits: SchemaField<{ + otherTags: ArrayField, string[], string[], true, false, true>; + }>; + _migration: SchemaField<{ + version: NumberField; + previous: SchemaField< + { + foundry: StringField; + system: StringField; + schema: NumberField; + }, + { foundry: string | null; system: string | null; schema: number | null }, + { foundry: string | null; system: string | null; schema: number | null }, + true, + true, + true + >; + }>; +}; + +export { ItemSystemModel, type ItemSystemSchema }; diff --git a/src/module/item/base/data/system.ts b/src/module/item/base/data/system.ts index 337eacefbd6..bca1cfe336a 100644 --- a/src/module/item/base/data/system.ts +++ b/src/module/item/base/data/system.ts @@ -84,7 +84,7 @@ type ItemSystemSource = { /** A record of this actor's current world schema version as well a log of the last migration to occur */ _migration: MigrationRecord; /** Legacy location of `MigrationRecord` */ - schema?: Readonly<{ version: number | null; lastMigration: object | null }>; + schema?: object; }; interface ItemDescriptionSource { @@ -92,7 +92,7 @@ interface ItemDescriptionSource { value: string; } -interface ItemSystemData extends ItemSystemSource { +interface ItemSystemData extends Omit { description: ItemDescriptionData; } diff --git a/src/module/item/base/document.ts b/src/module/item/base/document.ts index efa4e147b9e..5767c8860c2 100644 --- a/src/module/item/base/document.ts +++ b/src/module/item/base/document.ts @@ -68,7 +68,10 @@ class ItemPF2e extends Item /** The recorded schema version of this item, updated after each data migration */ get schemaVersion(): number | null { - return Number(this.system._migration?.version ?? this.system.schema?.version) || null; + const legacyValue = R.isPlainObject(this._source.system.schema) + ? Number(this._source.system.schema.version) || null + : null; + return Number(this._source.system._migration?.version) ?? legacyValue; } get description(): string { @@ -714,7 +717,9 @@ class ItemPF2e extends Item } // Remove any rule elements that request their own removal upon item creation - this._source.system.rules = this._source.system.rules.filter((r) => !r.removeUponCreate); + this._source.system.rules = this._source.system.rules.filter( + (r) => !("removeUponCreate" in r) || !r.removeUponCreate, + ); return super._preCreate(data, options, user); } diff --git a/src/module/item/kit/data.ts b/src/module/item/kit/data.ts index d4a144efe7b..2288eb0aa6c 100644 --- a/src/module/item/kit/data.ts +++ b/src/module/item/kit/data.ts @@ -1,24 +1,150 @@ -import type { BaseItemSourcePF2e, ItemSystemData, ItemSystemSource } from "@item/base/data/system.ts"; -import type { PartialPrice, PhysicalItemTrait, PhysicalItemTraits } from "@item/physical/data.ts"; +import type { ItemPF2e } from "@item"; +import { ItemSystemModel, ItemSystemSchema } from "@item/base/data/schema.ts"; +import type { BaseItemSourcePF2e, ItemSystemSource } from "@item/base/data/system.ts"; +import type { ClassTrait } from "@item/class/types.ts"; +import { PriceField } from "@item/physical/schema.ts"; +import type { RuleElementSource } from "@module/rules/index.ts"; +import { NullField, RecordField, SlugField } from "@system/schema-data-fields.ts"; +import type { + ArrayField, + BooleanField, + DocumentUUIDField, + FilePathField, + NumberField, + SchemaField, + StringField, +} from "types/foundry/common/data/fields.d.ts"; -type KitSource = BaseItemSourcePF2e<"kit", KitSystemSource>; +class KitEntriesField extends RecordField< + StringField, + SchemaField, + true, + false, + true, + true +> { + /** + * @param depth The recursion depth of this field:must be between 0 and 2 + */ + constructor(depth = 0) { + const fields = foundry.data.fields; + + type KitEntryValueSchema = { + uuid: DocumentUUIDField; + img: FilePathField; + quantity: NumberField; + name: StringField; + isContainer: BooleanField; + items: KitEntriesField | NullField; + }; + const hasNestedItems = depth <= 2; + const valueSchemaData = (): KitEntryValueSchema => ({ + uuid: new fields.DocumentUUIDField({ + required: true, + nullable: false, + initial: undefined, + }), + img: new fields.FilePathField({ + categories: ["IMAGE"], + base64: false, + required: true, + nullable: false, + initial: undefined, + }), + quantity: new fields.NumberField({ + required: true, + integer: true, + positive: true, + nullable: false, + initial: undefined, + }), + name: new fields.StringField({ required: true, nullable: false, blank: false, initial: undefined }), + isContainer: new fields.BooleanField({ + required: true, + nullable: false, + initial: undefined, + }), + items: hasNestedItems ? new KitEntriesField(depth + 1) : new NullField(), + }); -interface KitEntryData { - uuid: ItemUUID; - img: ImageFilePath; - quantity: number; - name: string; - isContainer: boolean; - items?: Record; + super( + new fields.StringField({ required: true, nullable: false, blank: false, initial: undefined }), + new fields.SchemaField(valueSchemaData(), { + required: true, + nullable: false, + initial: undefined, + }), + ); + } } -interface KitSystemSource extends ItemSystemSource { - traits: PhysicalItemTraits; - items: Record; - price: PartialPrice; - level?: never; +class KitSystemData extends ItemSystemModel { + static override defineSchema(): KitSystemSchema { + const fields = foundry.data.fields; + + const traitChoices: Record = { + ...CONFIG.PF2E.classTraits, + }; + + return { + ...super.defineSchema(), + traits: new fields.SchemaField({ + otherTags: new fields.ArrayField( + new SlugField({ required: true, nullable: false, initial: undefined }), + ), + value: new fields.ArrayField( + new fields.StringField({ + required: true, + nullable: false, + choices: traitChoices, + initial: undefined, + }), + ), + }), + items: new KitEntriesField(), + price: new PriceField(), + }; + } } -interface KitSystemData extends Omit, Omit {} +interface KitSystemData + extends ItemSystemModel, + Omit, "description"> {} + +type KitEntryData = NonNullable; + +type KitEntryValueSchema = { + uuid: DocumentUUIDField; + img: FilePathField; + quantity: NumberField; + name: StringField; + isContainer: BooleanField; + items: KitEntriesField | NullField; +}; + +type KitSystemSchema = Omit & { + traits: SchemaField<{ + otherTags: ArrayField, string[], string[], true, false, true>; + value: ArrayField< + StringField, + ClassTrait[], + ClassTrait[], + true, + false, + true + >; + }>; + items: KitEntriesField; + price: PriceField; +}; + +type KitSystemSource = Omit, "rules"> & { + level?: never; + schema?: ItemSystemSource["schema"]; + rules: RuleElementSource[]; +}; + +type KitSource = BaseItemSourcePF2e<"kit", KitSystemSource>; -export type { KitEntryData, KitSource, KitSystemData, KitSystemSource }; +export { KitSystemData }; +export type { KitEntryData, KitSource }; diff --git a/src/module/item/kit/document.ts b/src/module/item/kit/document.ts index dcb60e5442e..3e59439a967 100644 --- a/src/module/item/kit/document.ts +++ b/src/module/item/kit/document.ts @@ -3,13 +3,12 @@ import { ActorSizePF2e } from "@actor/data/size.ts"; import { ItemPF2e, type PhysicalItemPF2e } from "@item"; import type { ClassTrait } from "@item/class/types.ts"; import { Price } from "@item/physical/data.ts"; -import { CoinsPF2e } from "@item/physical/helpers.ts"; import { DENOMINATIONS } from "@item/physical/values.ts"; import { Size } from "@module/data.ts"; import type { UserPF2e } from "@module/user/index.ts"; import { ErrorPF2e, isObject } from "@util"; import { UUIDUtils } from "@util/uuid.ts"; -import { KitEntryData, KitSource, KitSystemData } from "./data.ts"; +import { KitSource, KitSystemData, type KitEntryData } from "./data.ts"; class KitPF2e extends ItemPF2e { static override get validTraits(): Record { @@ -21,10 +20,7 @@ class KitPF2e extends ItemP } get price(): Price { - return { - value: new CoinsPF2e(this.system.price.value), - per: this.system.price.per ?? 1, - }; + return this.system.price; } /** Expand a tree of kit entry data into a list of physical items */ diff --git a/src/module/item/physical/data.ts b/src/module/item/physical/data.ts index eda95aca675..7063246e70d 100644 --- a/src/module/item/physical/data.ts +++ b/src/module/item/physical/data.ts @@ -148,12 +148,12 @@ interface PhysicalItemHitPoints extends PhysicalItemHPSource { brokenThreshold: number; } -interface Coins { +type Coins = { pp?: number; gp?: number; sp?: number; cp?: number; -} +}; interface PartialPrice { value: Coins; diff --git a/src/module/item/physical/schema.ts b/src/module/item/physical/schema.ts new file mode 100644 index 00000000000..57d369ba627 --- /dev/null +++ b/src/module/item/physical/schema.ts @@ -0,0 +1,74 @@ +import type { NumberField, SchemaField } from "types/foundry/common/data/fields.d.ts"; +import { CoinsPF2e } from "./coins.ts"; + +const fields = foundry.data.fields; + +class PriceField extends fields.SchemaField { + constructor() { + const denominationField = (): NumberField => + new fields.NumberField({ required: false, nullable: false, initial: undefined }); + super( + { + value: new fields.SchemaField( + { + cp: denominationField(), + sp: denominationField(), + gp: denominationField(), + pp: denominationField(), + }, + { + required: true, + nullable: false, + }, + ), + per: new fields.NumberField({ + required: true, + nullable: false, + positive: true, + integer: true, + initial: 1, + }), + }, + { + required: true, + nullable: false, + initial: () => ({ + value: { + cp: undefined, + sp: undefined, + gp: undefined, + pp: undefined, + }, + per: 1, + }), + }, + ); + } + + override initialize(source: SourceFromSchema): PriceData { + const initialized = super.initialize(source); + initialized.value = new CoinsPF2e(initialized.value); + return initialized; + } +} + +type CoinsField = SchemaField, CoinsPF2e, true, false, true>; + +type CoinsSchema = { + cp: NumberField; + sp: NumberField; + gp: NumberField; + pp: NumberField; +}; + +type PriceSchema = { + value: CoinsField; + per: NumberField; +}; + +type PriceData = { + value: CoinsPF2e; + per: number; +}; + +export { PriceField }; diff --git a/src/module/migration/migrations/841-v11-uuid-format.ts b/src/module/migration/migrations/841-v11-uuid-format.ts index 7bda7130cab..a6ce003c36d 100644 --- a/src/module/migration/migrations/841-v11-uuid-format.ts +++ b/src/module/migration/migrations/841-v11-uuid-format.ts @@ -79,7 +79,7 @@ export class Migration841V11UUIDFormat extends MigrationBase { }); if (itemIsOfType(source, "ancestry", "background", "class", "kit")) { - const items: Record }> = + const items: Record | null }> = source.system.items; for (const entry of Object.values(items)) { entry.uuid = this.#replaceUUID(entry.uuid, "Item"); diff --git a/src/module/system/schema-data-fields.ts b/src/module/system/schema-data-fields.ts index 7217d76e3f7..064a73d177d 100644 --- a/src/module/system/schema-data-fields.ts +++ b/src/module/system/schema-data-fields.ts @@ -328,12 +328,27 @@ class PredicateField< type RecordFieldModelProp< TKeyField extends StringField | NumberField, TValueField extends DataField, -> = Partial, ModelPropFromDataField>>; + TDense extends boolean = false, +> = TDense extends true + ? Record, ModelPropFromDataField> + : TDense extends false + ? Partial, ModelPropFromDataField>> + : + | Record, ModelPropFromDataField> + | Partial, ModelPropFromDataField>>; type RecordFieldSourceProp< TKeyField extends StringField | NumberField, TValueField extends DataField, -> = Partial, SourcePropFromDataField>>; + /** Whether this is to be treated as a "dense" record; i.e., any valid key should return a value */ + TDense extends boolean = false, +> = TDense extends true + ? Record, SourcePropFromDataField> + : TDense extends false + ? Partial, SourcePropFromDataField>> + : + | Record, SourcePropFromDataField> + | Partial, SourcePropFromDataField>>; class RecordField< TKeyField extends StringField | NumberField, @@ -341,9 +356,10 @@ class RecordField< TRequired extends boolean = true, TNullable extends boolean = false, THasInitial extends boolean = true, + TDense extends boolean = false, > extends fields.ObjectField< - RecordFieldSourceProp, - RecordFieldModelProp, + RecordFieldSourceProp, + RecordFieldModelProp, TRequired, TNullable, THasInitial @@ -356,7 +372,12 @@ class RecordField< constructor( keyField: TKeyField, valueField: TValueField, - options: ObjectFieldOptions, TRequired, TNullable, THasInitial>, + options?: ObjectFieldOptions< + RecordFieldSourceProp, + TRequired, + TNullable, + THasInitial + >, ) { super(options); @@ -413,6 +434,7 @@ class RecordField< options?: CleanFieldOptions | undefined, ): Record { for (const [key, value] of Object.entries(values)) { + if (key.startsWith("-=")) continue; // Don't attempt to clean deletion entries values[key] = this.valueField.clean(value, options); } return values; @@ -432,24 +454,36 @@ class RecordField< values: object | null | undefined, model: ConstructorOf, options?: ObjectFieldOptions, TRequired, TNullable, THasInitial>, - ): MaybeSchemaProp, TRequired, TNullable, THasInitial>; + ): MaybeSchemaProp, TRequired, TNullable, THasInitial>; override initialize( values: object | null | undefined, model: ConstructorOf, options?: ObjectFieldOptions, TRequired, TNullable, THasInitial>, - ): RecordFieldModelProp | null | undefined { + ): Record | null | undefined { if (!values) return values; const data: Record = {}; for (const [key, value] of Object.entries(values)) { data[key] = this.valueField.initialize(value, model, options); } - return data as RecordFieldModelProp; + return data; + } +} + +/** A field that always results in a value of `null` */ +class NullField extends fields.DataField { + constructor() { + super({ required: true, nullable: true, initial: null }); + } + + protected override _cast(): null { + return null; } } export { DataUnionField, LaxSchemaField, + NullField, PredicateField, RecordField, SlugField, diff --git a/src/scripts/hooks/init.ts b/src/scripts/hooks/init.ts index e9f13de5a59..23b90294605 100644 --- a/src/scripts/hooks/init.ts +++ b/src/scripts/hooks/init.ts @@ -1,4 +1,5 @@ import { MystifiedTraits } from "@item/base/data/values.ts"; +import { KitSystemData } from "@item/kit/data.ts"; import { HotbarPF2e } from "@module/apps/hotbar.ts"; import { ActorDirectoryPF2e, @@ -36,6 +37,8 @@ export const Init = { CONFIG.PF2E = PF2ECONFIG; CONFIG.debug.ruleElement ??= false; + CONFIG.Item.dataModels.kit = KitSystemData; + // Assign canvas layer and placeable classes CONFIG.AmbientLight.layerClass = LightingLayerPF2e; CONFIG.AmbientLight.objectClass = AmbientLightPF2e; diff --git a/types/foundry/client/config.d.ts b/types/foundry/client/config.d.ts index 7f83d6b5103..1768379b57f 100644 --- a/types/foundry/client/config.d.ts +++ b/types/foundry/client/config.d.ts @@ -7,6 +7,8 @@ import type { PointVisionSource, } from "../client-esm/canvas/sources/module.ts"; import type * as terms from "../client-esm/dice/terms/module.d.ts"; +import TypeDataModel from "../common/abstract/type-data.js"; +import { DataSchema } from "../common/data/fields.js"; declare global { interface Config< @@ -117,6 +119,9 @@ declare global { new (data: PreCreate, context?: DocumentConstructionContext): TItem; }; collection: typeof Items; + dataModels: Record>>; + typeIcons: Record; + typeLabels: Record; sheetClasses: Record< string, Record< @@ -131,8 +136,6 @@ declare global { } > >; - typeIcons: Record; - typeLabels: Record; }; /** Configuration for the Combat document */