@vttforge/core 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,982 @@
1
+ //#region src/base-actor-sheet.d.ts
2
+ /**
3
+ * BaseActorSheet — `ActorSheetV2 + HandlebarsApplicationMixin` baseline with the
4
+ * boilerplate every shipping system copy-pastes hoisted into the SDK.
5
+ *
6
+ * What this adds beyond stock Foundry v13:
7
+ *
8
+ * - **`static DRAG_DROP`** — declare drag sources / drop targets as data, get
9
+ * `foundry.applications.ux.DragDrop` instances wired in `_onRender` with
10
+ * `isEditable`-gated permissions and a sensible default `_onDragStart` that
11
+ * serialises `data-item-id` elements as `{ type: "Item", uuid }`.
12
+ * - **`_prepareContext` auto-fills `context.tabs[group]`** for every group
13
+ * declared in ApplicationV2's `static TABS`, so subclass `_prepareContext`
14
+ * implementations stop having to call `_prepareTabs(group)` by hand.
15
+ * - **Typed drop dispatch** — override `onDropItem(item, event)` /
16
+ * `onDropActor(actor, event)` / `onDropFolder(folder, event)` /
17
+ * `onDropActiveEffect(effect, event)` and skip the `fromUuid()` ceremony.
18
+ * Returning `undefined` falls through to Foundry's default `_onDropX`
19
+ * behaviour; return any other value to take ownership.
20
+ *
21
+ * Intentional non-additions:
22
+ *
23
+ * - `editImage` action — already shipped by `DocumentSheetV2` (inherited by
24
+ * `ActorSheetV2`). Templates wire `<img data-edit="img">` and Foundry's
25
+ * built-in action handles the `FilePicker` flow.
26
+ * - `_getTabs()` — ApplicationV2 already owns the tab state machine; we only
27
+ * eliminate the `_prepareTabs` call in `_prepareContext`.
28
+ *
29
+ * Resolved lazily so subclasses can be declared at module load without
30
+ * Foundry globals existing yet (test boot, ESM hoist).
31
+ */
32
+ /**
33
+ * Declarative DragDrop entry consumed by `_onRender`. Mirrors the
34
+ * `foundry.applications.ux.DragDrop` constructor config. Permissions and
35
+ * callbacks fall back to sensible defaults that honour `this.isEditable` and
36
+ * the default `_onDragStart` / `_onDrop`.
37
+ */
38
+ interface DragDropConfig {
39
+ readonly dragSelector?: string;
40
+ readonly dropSelector?: string;
41
+ readonly permissions?: {
42
+ readonly dragstart?: () => boolean;
43
+ readonly drop?: () => boolean;
44
+ };
45
+ readonly callbacks?: Record<string, (...args: any[]) => unknown>;
46
+ }
47
+ /**
48
+ * The statics a VTTForge sheet base carries.
49
+ *
50
+ * The factory used to return a bare constructor, so a subclass writing
51
+ * `super.DEFAULT_OPTIONS` — the pattern the docs show and every sheet needs —
52
+ * failed to compile. TypeScript cannot see a static through an untyped
53
+ * constructor. The example system never caught it because it is JavaScript.
54
+ *
55
+ * `DEFAULT_OPTIONS` is deliberately loose: a subclass merges its own shape
56
+ * into it, and pinning ours would reject the merge.
57
+ */
58
+ interface SheetBaseStatics {
59
+ readonly DEFAULT_OPTIONS: Record<string, any>;
60
+ readonly DRAG_DROP: ReadonlyArray<DragDropConfig>;
61
+ }
62
+ /**
63
+ * What the factory hands back: something you can `extend`, whose statics the
64
+ * compiler can see.
65
+ */
66
+ interface SheetBaseCtor extends SheetBaseStatics {
67
+ new (...args: any[]): any;
68
+ }
69
+ /**
70
+ * Marker class that consumer CSS uses for scoping. Always present on every
71
+ * VTTForge-derived sheet so rules like `.vttforge .actor-sheet { ... }` work.
72
+ */
73
+ declare const VTTFORGE_SHEET_CLASS = "vttforge";
74
+ /**
75
+ * Build the `BaseActorSheet` for the current Foundry runtime. See module
76
+ * header for the boilerplate this base eliminates.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * class CharacterSheet extends BaseActorSheet() {
81
+ * static DEFAULT_OPTIONS = foundry.utils.mergeObject(
82
+ * super.DEFAULT_OPTIONS,
83
+ * { classes: ['my-system'], position: { width: 720 } },
84
+ * );
85
+ * static PARTS = { ... };
86
+ * static TABS = {
87
+ * primary: {
88
+ * tabs: [
89
+ * { id: 'features', group: 'primary', label: 'Features' },
90
+ * { id: 'inventory', group: 'primary', label: 'Inventory' },
91
+ * ],
92
+ * initial: 'features',
93
+ * },
94
+ * };
95
+ * static DRAG_DROP = [{ dragSelector: '.item[draggable=true]', dropSelector: null }];
96
+ * async onDropItem(item, event) {
97
+ * if (item.type !== 'weapon') return false;
98
+ * // …fall through to super by returning undefined.
99
+ * }
100
+ * }
101
+ * ```
102
+ */
103
+ declare function BaseActorSheet(): SheetBaseCtor;
104
+ //#endregion
105
+ //#region src/base-item-sheet.d.ts
106
+ /**
107
+ * Build the `BaseItemSheet` for the current Foundry runtime.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * class WeaponSheet extends BaseItemSheet() {
112
+ * static DEFAULT_OPTIONS = foundry.utils.mergeObject(
113
+ * super.DEFAULT_OPTIONS,
114
+ * { classes: ['my-system'], position: { width: 540 } },
115
+ * );
116
+ * static PARTS = { ... };
117
+ * static TABS = {
118
+ * primary: {
119
+ * tabs: [
120
+ * { id: 'description', group: 'primary', label: 'Description' },
121
+ * { id: 'details', group: 'primary', label: 'Details' },
122
+ * ],
123
+ * initial: 'description',
124
+ * },
125
+ * };
126
+ * }
127
+ * ```
128
+ */
129
+ declare function BaseItemSheet(): SheetBaseCtor;
130
+ //#endregion
131
+ //#region src/data/field-options.d.ts
132
+ /**
133
+ * Option interfaces for the Foundry v13 data fields covered by `@vttforge/core`'s
134
+ * v0.1 `InferSchema<T>` surface.
135
+ *
136
+ * Mirrors the shape documented at https://foundryvtt.com/api/v13/ for each
137
+ * `foundry.data.fields.*` class. Properties are structural — they exist purely
138
+ * so the conditional types in `./infer-schema.ts` can extract semantics like
139
+ * `nullable: true` without us pulling in `fvtt-types` (deferred to
140
+ * `@vttforge/types` v1.0).
141
+ */
142
+ /**
143
+ * Properties shared by every Foundry data field option object.
144
+ *
145
+ * Mirrors the base `DataFieldOptions` interface documented at
146
+ * https://foundryvtt.com/api/v13/classes/foundry.data.fields.DataField.html.
147
+ */
148
+ interface DataFieldOptions {
149
+ readonly required?: boolean;
150
+ readonly nullable?: boolean;
151
+ readonly initial?: unknown;
152
+ readonly readonly?: boolean;
153
+ readonly gmOnly?: boolean;
154
+ readonly label?: string;
155
+ readonly hint?: string;
156
+ readonly validationError?: string;
157
+ readonly validate?: (value: unknown) => boolean | undefined;
158
+ }
159
+ interface NumberFieldOptions extends DataFieldOptions {
160
+ readonly integer?: boolean;
161
+ readonly positive?: boolean;
162
+ readonly min?: number;
163
+ readonly max?: number;
164
+ readonly step?: number;
165
+ readonly choices?: readonly number[] | Record<string, number>;
166
+ }
167
+ interface StringFieldOptions extends DataFieldOptions {
168
+ readonly blank?: boolean;
169
+ readonly trim?: boolean;
170
+ readonly textSearch?: boolean;
171
+ readonly choices?: readonly string[] | Record<string, string>;
172
+ }
173
+ type BooleanFieldOptions = DataFieldOptions;
174
+ type HTMLFieldOptions = StringFieldOptions;
175
+ type ColorFieldOptions = StringFieldOptions;
176
+ interface FilePathFieldOptions extends StringFieldOptions {
177
+ readonly categories?: ReadonlyArray<'IMAGE' | 'VIDEO' | 'AUDIO' | 'TEXT' | 'FONT' | 'GRAPHICS'>;
178
+ readonly base64?: boolean;
179
+ readonly wildcard?: boolean;
180
+ }
181
+ interface ArrayFieldOptions extends DataFieldOptions {
182
+ readonly min?: number;
183
+ readonly max?: number;
184
+ }
185
+ type SchemaFieldOptions = DataFieldOptions;
186
+ /**
187
+ * `SetField` takes the same options as `ArrayField` — it is a subclass whose
188
+ * only difference is what `initialize` hands back.
189
+ */
190
+ type SetFieldOptions = ArrayFieldOptions;
191
+ interface ForeignDocumentFieldOptions extends DataFieldOptions {
192
+ /**
193
+ * Keep the stored id instead of resolving the document.
194
+ *
195
+ * With this off, the field initializes to a getter — reading the property
196
+ * gives you a function, and calling it looks the document up. That is why
197
+ * the two cases infer to different types.
198
+ */
199
+ readonly idOnly?: boolean;
200
+ }
201
+ //#endregion
202
+ //#region src/data/fields.d.ts
203
+ declare const BRAND: unique symbol;
204
+ /**
205
+ * Anything that satisfies the `FieldInstance` shape — used as the inner-field
206
+ * constraint on `ArrayField` and as the value type of `SchemaField`'s child
207
+ * map. Keeps the conditional types in `./infer-schema.ts` straightforward.
208
+ */
209
+ interface FieldInstance {
210
+ readonly [BRAND]: string;
211
+ readonly options: unknown;
212
+ }
213
+ interface NumberFieldInstance<O extends NumberFieldOptions = NumberFieldOptions> extends FieldInstance {
214
+ readonly [BRAND]: 'number';
215
+ readonly options: O;
216
+ }
217
+ interface StringFieldInstance<O extends StringFieldOptions = StringFieldOptions> extends FieldInstance {
218
+ readonly [BRAND]: 'string';
219
+ readonly options: O;
220
+ }
221
+ interface BooleanFieldInstance<O extends BooleanFieldOptions = BooleanFieldOptions> extends FieldInstance {
222
+ readonly [BRAND]: 'boolean';
223
+ readonly options: O;
224
+ }
225
+ interface HTMLFieldInstance<O extends HTMLFieldOptions = HTMLFieldOptions> extends FieldInstance {
226
+ readonly [BRAND]: 'html';
227
+ readonly options: O;
228
+ }
229
+ interface ColorFieldInstance<O extends ColorFieldOptions = ColorFieldOptions> extends FieldInstance {
230
+ readonly [BRAND]: 'color';
231
+ readonly options: O;
232
+ }
233
+ interface FilePathFieldInstance<O extends FilePathFieldOptions = FilePathFieldOptions> extends FieldInstance {
234
+ readonly [BRAND]: 'filePath';
235
+ readonly options: O;
236
+ }
237
+ interface ArrayFieldInstance<Inner extends FieldInstance = FieldInstance, O extends ArrayFieldOptions = ArrayFieldOptions> extends FieldInstance {
238
+ readonly [BRAND]: 'array';
239
+ readonly element: Inner;
240
+ readonly options: O;
241
+ }
242
+ /**
243
+ * A `SetField` holds a `Set`, not an array.
244
+ *
245
+ * It extends `ArrayField` and validates the same way, but `initialize`
246
+ * wraps the result in `new Set(...)` — so a schema that declares one and
247
+ * types it as an array gets `.push` and index access from the compiler on a
248
+ * value that has neither.
249
+ */
250
+ interface SetFieldInstance<Inner extends FieldInstance = FieldInstance, O extends SetFieldOptions = SetFieldOptions> extends FieldInstance {
251
+ readonly [BRAND]: 'set';
252
+ readonly element: Inner;
253
+ readonly options: O;
254
+ }
255
+ /**
256
+ * A reference to another document, stored as its id.
257
+ *
258
+ * What you read back depends on `idOnly`. With it, the id string. Without
259
+ * it, the document itself: the field resolves to a getter, so reading the
260
+ * property looks the document up in its collection and hands back the
261
+ * instance — or `null` when it is gone or lives in a compendium.
262
+ *
263
+ * The field is nullable by default, so both shapes admit `null`.
264
+ */
265
+ interface ForeignDocumentFieldInstance<Doc extends DocumentClass = DocumentClass, O extends ForeignDocumentFieldOptions = ForeignDocumentFieldOptions> extends FieldInstance {
266
+ readonly [BRAND]: 'foreignDocument';
267
+ readonly model: Doc;
268
+ readonly options: O;
269
+ }
270
+ interface SchemaFieldInstance<S extends Record<string, FieldInstance> = Record<string, FieldInstance>, O extends SchemaFieldOptions = SchemaFieldOptions> extends FieldInstance {
271
+ readonly [BRAND]: 'schema';
272
+ readonly fields: S;
273
+ readonly options: O;
274
+ }
275
+ interface NumberFieldCtor {
276
+ new <O extends NumberFieldOptions = NumberFieldOptions>(options?: O): NumberFieldInstance<O>;
277
+ }
278
+ interface StringFieldCtor {
279
+ new <O extends StringFieldOptions = StringFieldOptions>(options?: O): StringFieldInstance<O>;
280
+ }
281
+ interface BooleanFieldCtor {
282
+ new <O extends BooleanFieldOptions = BooleanFieldOptions>(options?: O): BooleanFieldInstance<O>;
283
+ }
284
+ interface HTMLFieldCtor {
285
+ new <O extends HTMLFieldOptions = HTMLFieldOptions>(options?: O): HTMLFieldInstance<O>;
286
+ }
287
+ interface ColorFieldCtor {
288
+ new <O extends ColorFieldOptions = ColorFieldOptions>(options?: O): ColorFieldInstance<O>;
289
+ }
290
+ interface FilePathFieldCtor {
291
+ new <O extends FilePathFieldOptions = FilePathFieldOptions>(options?: O): FilePathFieldInstance<O>;
292
+ }
293
+ interface ArrayFieldCtor {
294
+ new <Inner extends FieldInstance, O extends ArrayFieldOptions = ArrayFieldOptions>(element: Inner, options?: O): ArrayFieldInstance<Inner, O>;
295
+ }
296
+ interface SetFieldCtor {
297
+ new <Inner extends FieldInstance, O extends SetFieldOptions = SetFieldOptions>(element: Inner, options?: O): SetFieldInstance<Inner, O>;
298
+ }
299
+ /**
300
+ * Any document class — what `ForeignDocumentField` takes as its first
301
+ * argument. Declared structurally so the inference surface stays free of a
302
+ * dependency on a Foundry type package.
303
+ */
304
+ type DocumentClass = abstract new (...args: never[]) => object;
305
+ interface ForeignDocumentFieldCtor {
306
+ new <Doc extends DocumentClass, O extends ForeignDocumentFieldOptions = ForeignDocumentFieldOptions>(model: Doc, options?: O): ForeignDocumentFieldInstance<Doc, O>;
307
+ }
308
+ interface SchemaFieldCtor {
309
+ new <S extends Record<string, FieldInstance>, O extends SchemaFieldOptions = SchemaFieldOptions>(fields: S, options?: O): SchemaFieldInstance<S, O>;
310
+ }
311
+ /**
312
+ * Typed bag returned by `fields()`. Each property is the corresponding
313
+ * `foundry.data.fields.*` class — the runtime value is Foundry's own
314
+ * constructor; the type is our overlay.
315
+ */
316
+ interface FieldsApi {
317
+ readonly NumberField: NumberFieldCtor;
318
+ readonly StringField: StringFieldCtor;
319
+ readonly BooleanField: BooleanFieldCtor;
320
+ readonly HTMLField: HTMLFieldCtor;
321
+ readonly ColorField: ColorFieldCtor;
322
+ readonly FilePathField: FilePathFieldCtor;
323
+ readonly ArrayField: ArrayFieldCtor;
324
+ readonly SetField: SetFieldCtor;
325
+ readonly ForeignDocumentField: ForeignDocumentFieldCtor;
326
+ readonly SchemaField: SchemaFieldCtor;
327
+ }
328
+ /**
329
+ * Resolve `globalThis.foundry.data.fields` and return it typed as `FieldsApi`.
330
+ *
331
+ * Call this inside `defineSchema()` (or any code that runs after Foundry's
332
+ * `init` hook). Calling at module scope will throw when imported from Node
333
+ * tests — the global only exists inside the Foundry runtime.
334
+ *
335
+ * @throws `VttfError` with code `VTTF-0002` when `foundry.data.fields` is
336
+ * missing.
337
+ */
338
+ declare function fields(): FieldsApi;
339
+ //#endregion
340
+ //#region src/data/color.d.ts
341
+ /**
342
+ * The shape a `ColorField` hands back.
343
+ *
344
+ * Foundry initializes the field into one of its own `Color` instances — a
345
+ * boxed 24-bit integer with derived accessors — rather than the CSS string it
346
+ * stores. Described structurally here rather than imported, because the
347
+ * inference surface deliberately does not depend on the Foundry type package.
348
+ *
349
+ * Members mirror the documented public accessors. `toString(radix)` is
350
+ * included because a colour is routinely interpolated straight into markup.
351
+ */
352
+ interface Color {
353
+ /** False when the underlying value is not a valid colour. */
354
+ readonly valid: boolean;
355
+ /** CSS hexadecimal string, e.g. `#ff0000`. */
356
+ readonly css: string;
357
+ /** Normalized `[r, g, b]`, each 0–1. */
358
+ readonly rgb: [number, number, number];
359
+ readonly r: number;
360
+ readonly g: number;
361
+ readonly b: number;
362
+ /** The largest of the three channels. */
363
+ readonly maximum: number;
364
+ /** The smallest of the three channels. */
365
+ readonly minimum: number;
366
+ /** Byte order flipped, for APIs that want BGR. */
367
+ readonly littleEndian: number;
368
+ readonly hsv: [number, number, number];
369
+ readonly hsl: [number, number, number];
370
+ /** The colour in linear space, for shader work. */
371
+ readonly linear: Color;
372
+ toString: (radix?: number) => string;
373
+ valueOf: () => number;
374
+ }
375
+ //#endregion
376
+ //#region src/data/infer-schema.d.ts
377
+ /**
378
+ * Flatten an intersection / mapped type into a plain object literal so IDE
379
+ * hovers stay readable (Matt Pocock's `Prettify`). Use on every public
380
+ * conditional-type surface — PRD §7 TS-hygiene rule.
381
+ */
382
+ type Prettify<T> = { [K in keyof T]: T[K]; } & {};
383
+ /**
384
+ * Whether a field's options gave an explicit `initial`.
385
+ *
386
+ * Presence of the key is the question, not its value — `{ initial: undefined }`
387
+ * is not an initial.
388
+ */
389
+ type HasInitial<O> = O extends {
390
+ initial: unknown;
391
+ } ? true : false;
392
+ /**
393
+ * What a field class decides on its own behalf, for the options a schema
394
+ * leaves out.
395
+ *
396
+ * Every field type sets its own defaults, and they disagree. A number field
397
+ * is optional and nullable out of the box; a boolean field is required and
398
+ * starts at `false`. Reading a schema without knowing which defaults apply
399
+ * gets the shape wrong for exactly the fields people write most.
400
+ */
401
+ interface FieldDefaults {
402
+ /** Whether the field is required when the schema does not say. */
403
+ required: boolean;
404
+ /** Whether the field admits `null` when the schema does not say. */
405
+ nullable: boolean;
406
+ /** Whether the field supplies a value when the schema gives no `initial`. */
407
+ populated: boolean;
408
+ }
409
+ /** Read an option the schema may have set, falling back to the field's default. */
410
+ type Resolve<O, Key extends string, Fallback extends boolean> = O extends Record<Key, true> ? true : O extends Record<Key, false> ? false : Fallback;
411
+ /**
412
+ * Widen a field's base type by what it can actually hold.
413
+ *
414
+ * Two independent widenings:
415
+ *
416
+ * - nullable admits `null`.
417
+ * - not required, with nothing to fall back on, admits `undefined`: cleaning
418
+ * asks the field for an initial value and keeps whatever it gets, which is
419
+ * `undefined` when there is no initial to give.
420
+ *
421
+ * A required field never widens to `undefined`. It would fail validation
422
+ * before the document existed, so there is no state to type.
423
+ */
424
+ type Presence<O, T, D extends FieldDefaults> = T | (Resolve<O, 'nullable', D['nullable']> extends true ? null : never) | (Resolve<O, 'required', D['required']> extends true ? never : HasInitial<O> extends true ? never : D['populated'] extends true ? never : undefined);
425
+ /** Optional, nullable, nothing to fall back on. */
426
+ type NumberDefaults = {
427
+ required: false;
428
+ nullable: true;
429
+ populated: false;
430
+ };
431
+ /** Optional and non-nullable, so an unset one is simply absent. */
432
+ type StringDefaults = {
433
+ required: false;
434
+ nullable: false;
435
+ populated: false;
436
+ };
437
+ /** Required, and starts at `false`. */
438
+ type BooleanDefaults = {
439
+ required: true;
440
+ nullable: false;
441
+ populated: true;
442
+ };
443
+ /** Required and blank-friendly, so an unset one is the empty string. */
444
+ type HTMLDefaults = {
445
+ required: true;
446
+ nullable: false;
447
+ populated: true;
448
+ };
449
+ /** Optional and nullable, but starts at `null` rather than absent. */
450
+ type NullStartDefaults = {
451
+ required: false;
452
+ nullable: true;
453
+ populated: true;
454
+ };
455
+ /** Required, and builds its own empty value. */
456
+ type ContainerDefaults = {
457
+ required: true;
458
+ nullable: false;
459
+ populated: true;
460
+ };
461
+ /** Required but nullable — an id that points at nothing is `null`. */
462
+ type ReferenceDefaults = {
463
+ required: true;
464
+ nullable: true;
465
+ populated: false;
466
+ };
467
+ /**
468
+ * What a `ColorField` holds once the model is initialized.
469
+ *
470
+ * Not a string. The field casts its stored value to a CSS string, but
471
+ * `initialize` hands back a `Color` instance — so `system.tint` is an object
472
+ * with `.css`, `.rgb`, `.hex` and friends, and typing it as `string` makes
473
+ * every property access on it a lie the compiler accepts.
474
+ *
475
+ * It starts at `null`, so reading `.css` off a fresh document crashes unless
476
+ * the schema gives it an initial. That is what this type refuses.
477
+ */
478
+ type ColorFieldValue<O> = Presence<O, Color, NullStartDefaults>;
479
+ /**
480
+ * What a `ForeignDocumentField` holds once the model is initialized.
481
+ *
482
+ * With `idOnly`, the stored id string. Without it the field resolves to a
483
+ * getter, and the data model installs it as one — so reading the property
484
+ * gives the document instance, not the function that fetched it. It yields
485
+ * `null` when the id points at nothing, or when the parent lives in a
486
+ * compendium.
487
+ *
488
+ * Two caveats this does not encode, both narrow enough to document rather
489
+ * than type:
490
+ *
491
+ * - On the server the field keeps the id string in both cases, because there
492
+ * are no collections to resolve against. System code runs on both sides,
493
+ * but typing that union would put a string check in front of every read of
494
+ * a document reference.
495
+ * - Under `readonly: true` the data model takes the read-only branch before
496
+ * the getter branch, so the property keeps the resolver function itself.
497
+ * The field turns that flag off by default and no schema has reason to
498
+ * turn it back on.
499
+ */
500
+ type ForeignDocumentValue<Doc extends DocumentClass, O> = Presence<O, O extends {
501
+ idOnly: true;
502
+ } ? string : InstanceType<Doc>, ReferenceDefaults>;
503
+ /**
504
+ * Map a single field instance to its runtime TypeScript type. `never` for
505
+ * shapes we don't recognise — the v1.0 `@vttforge/types` package will widen
506
+ * this matrix to the remaining Foundry fields.
507
+ */
508
+ type InferField<F> = F extends NumberFieldInstance<infer O> ? Presence<O, number, NumberDefaults> : F extends StringFieldInstance<infer O> ? Presence<O, string, StringDefaults> : F extends BooleanFieldInstance<infer O> ? Presence<O, boolean, BooleanDefaults> : F extends HTMLFieldInstance<infer O> ? Presence<O, string, HTMLDefaults> : F extends ColorFieldInstance<infer O> ? ColorFieldValue<O> : F extends FilePathFieldInstance<infer O> ? Presence<O, string, NullStartDefaults> : F extends ForeignDocumentFieldInstance<infer Doc, infer O> ? ForeignDocumentValue<Doc, O> : F extends ArrayFieldInstance<infer Inner, infer O> ? Presence<O, InferField<Inner>[], ContainerDefaults> : F extends SetFieldInstance<infer Inner, infer O> ? Presence<O, Set<InferField<Inner>>, ContainerDefaults> : F extends SchemaFieldInstance<infer S, infer O> ? Presence<O, InferSchema<S>, ContainerDefaults> : never;
509
+ /**
510
+ * Map a `defineSchema()` return value to the corresponding `system` shape.
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * class CharacterData extends BaseTypeDataModel() {
515
+ * static defineSchema() {
516
+ * const f = fields();
517
+ * return {
518
+ * level: new f.NumberField({ required: true, initial: 1 }),
519
+ * biography: new f.HTMLField(),
520
+ * };
521
+ * }
522
+ * }
523
+ * type CharacterSystem = InferSchema<ReturnType<typeof CharacterData.defineSchema>>;
524
+ * // → { level: number; biography: string }
525
+ * ```
526
+ */
527
+ type InferSchema<S extends Record<string, FieldInstance>> = Prettify<{ [K in keyof S]: InferField<S[K]>; }>;
528
+ //#endregion
529
+ //#region src/base-type-data-model.d.ts
530
+ type AnyConstructor = new (...args: any[]) => any;
531
+ /**
532
+ * Resolve the runtime base class, then build a mixin that adds VTTForge defaults.
533
+ *
534
+ * Why a function: subclasses are declared once at module load, but Foundry
535
+ * globals may not exist yet (test boot, ESM hoist). Calling `BaseTypeDataModel()`
536
+ * lazy-resolves the global at the moment of subclassing.
537
+ */
538
+ /** The two hooks this base fills in, so a subclass can omit either. */
539
+ interface TypeDataModelHooks {
540
+ prepareBaseData(): void;
541
+ prepareDerivedData(): void;
542
+ }
543
+ /**
544
+ * What an instance looks like when the schema is known.
545
+ *
546
+ * The schema's fields ARE the instance properties — inside
547
+ * `prepareDerivedData()` you read `this.level`, not `this.system.level`, and
548
+ * `actor.system` is this instance.
549
+ *
550
+ * Derived values are not in the schema, so they are not here either. Declare
551
+ * them on the subclass:
552
+ *
553
+ * ```ts
554
+ * declare armorClass: number;
555
+ * ```
556
+ */
557
+ type TypedTypeDataModel<S extends Record<string, FieldInstance>> = InferSchema<S> & TypeDataModelHooks & {
558
+ /**
559
+ * Phantom property carrying the schema's inferred shape. Never assigned,
560
+ * never present at runtime — it exists so the type has a name:
561
+ *
562
+ * ```ts
563
+ * type CharacterSystem = CharacterData['$inferData'];
564
+ * ```
565
+ */
566
+ readonly $inferData: InferSchema<S>;
567
+ };
568
+ interface TypedTypeDataModelCtor<S extends Record<string, FieldInstance>> {
569
+ new (...args: any[]): TypedTypeDataModel<S>;
570
+ defineSchema(): S;
571
+ migrateData(data: Record<string, unknown>): Record<string, unknown>;
572
+ }
573
+ /**
574
+ * Build a base class with no knowledge of the schema.
575
+ *
576
+ * `this` inside the hooks is untyped. Pass your schema function instead to
577
+ * get the fields typed.
578
+ */
579
+ declare function BaseTypeDataModel(): AnyConstructor;
580
+ /**
581
+ * Build a base class that knows its schema.
582
+ *
583
+ * Hand it the function that returns your fields and it implements
584
+ * `static defineSchema()` for you, so the schema is written once:
585
+ *
586
+ * ```ts
587
+ * const defineCharacterSchema = () => {
588
+ * const f = fields();
589
+ * return { level: new f.NumberField({ required: true, nullable: false, initial: 1 }) };
590
+ * };
591
+ *
592
+ * class CharacterData extends BaseTypeDataModel(defineCharacterSchema) {
593
+ * declare armorClass: number;
594
+ * prepareDerivedData() {
595
+ * this.armorClass = 10 + this.level; // this.level is number
596
+ * }
597
+ * }
598
+ * ```
599
+ *
600
+ * It has to be a function, not an object: `fields()` reads a Foundry global
601
+ * that does not exist when the module is first evaluated.
602
+ *
603
+ * A subclass may still declare its own `static defineSchema()`; that one wins,
604
+ * the same as any other static.
605
+ */
606
+ declare function BaseTypeDataModel<S extends Record<string, FieldInstance>>(defineSchema: () => S): TypedTypeDataModelCtor<S>;
607
+ //#endregion
608
+ //#region src/errors/registry.d.ts
609
+ /**
610
+ * VTTF-NNNN error registry — append-only, stable across majors.
611
+ *
612
+ * Every error VTTForge throws has a numeric code (`VTTF-NNNN`) and a PascalCase
613
+ * `name` for stack-trace readability. Codes are URLs — `https://vttforge.dev/errors/VTTF-0001`
614
+ * eventually links to a docs page generated from this registry.
615
+ *
616
+ * Never renumber an entry. To deprecate, mark with `deprecated: true` and add a
617
+ * `replacedBy` pointer. Adding a new code: pick the next unused integer.
618
+ */
619
+ type VttfErrorCode = `VTTF-${string}`;
620
+ interface VttfErrorEntry {
621
+ readonly code: VttfErrorCode;
622
+ readonly name: string;
623
+ readonly summary: string;
624
+ readonly deprecated?: boolean;
625
+ readonly replacedBy?: VttfErrorCode;
626
+ }
627
+ /**
628
+ * Look up a registered entry by code. Throws if the code is unknown — the
629
+ * registry is the source of truth, so missing codes mean a typo.
630
+ */
631
+ declare function getErrorEntry(code: VttfErrorCode): VttfErrorEntry;
632
+ /**
633
+ * Return every entry currently in the registry. Used by codegen to emit the
634
+ * runtime constants and the JSON manifest that powers the docs pages.
635
+ */
636
+ declare function listErrorEntries(): readonly VttfErrorEntry[];
637
+ declare function docsUrlFor(code: VttfErrorCode): string;
638
+ /**
639
+ * VttfError — every error VTTForge throws extends this.
640
+ *
641
+ * - `code` is the registry key (string-narrowed).
642
+ * - `name` is the PascalCase name from the registry — shows up in stack traces.
643
+ * - `docsUrl` points at the docs page.
644
+ * - `cause` uses the native ES2022 mechanism. Multiple causes => pass an
645
+ * `AggregateError` as the cause.
646
+ */
647
+ declare class VttfError extends Error {
648
+ readonly code: VttfErrorCode;
649
+ readonly docsUrl: string;
650
+ constructor(code: VttfErrorCode, message?: string, options?: ErrorOptions);
651
+ }
652
+ //#endregion
653
+ //#region src/errors/manifest.d.ts
654
+ declare const ERROR_MANIFEST_VERSION: 1;
655
+ interface ErrorManifest {
656
+ readonly version: typeof ERROR_MANIFEST_VERSION;
657
+ readonly package: '@vttforge/core';
658
+ readonly entries: ReadonlyArray<VttfErrorEntry>;
659
+ }
660
+ /**
661
+ * Snapshot the current registry as a manifest object. Recomputed on every
662
+ * call — cheap (the registry is a frozen literal). For the JSON projection
663
+ * shipped with the package, see `dist/errors-manifest.json`.
664
+ */
665
+ declare function getErrorManifest(): ErrorManifest;
666
+ //#endregion
667
+ //#region src/foundry-globals.d.ts
668
+ /**
669
+ * Minimal type-only contracts for the Foundry v13+ globals VTTForge core touches.
670
+ *
671
+ * Intentionally narrow — full Foundry typing lives in `@vttforge/types` (v1.0)
672
+ * built on top of `fvtt-types`. We mirror just the surface we use so the core
673
+ * package compiles without pulling in fvtt-types' git-SHA dependency.
674
+ *
675
+ * Every consumer is expected to run inside the Foundry runtime; we read these
676
+ * via `globalThis` and never bundle Foundry itself.
677
+ */
678
+ type HookCallback<Args extends readonly unknown[] = readonly unknown[]> = (...args: Args) => unknown | Promise<unknown>;
679
+ interface HooksApi {
680
+ once<Args extends readonly unknown[]>(event: string, fn: HookCallback<Args>): number;
681
+ on<Args extends readonly unknown[]>(event: string, fn: HookCallback<Args>): number;
682
+ off(event: string, idOrFn: number | HookCallback): boolean;
683
+ call(event: string, ...args: readonly unknown[]): boolean;
684
+ callAll(event: string, ...args: readonly unknown[]): boolean;
685
+ }
686
+ type SettingScope = 'world' | 'client';
687
+ interface SettingConfig<T = unknown> {
688
+ readonly name?: string;
689
+ readonly hint?: string;
690
+ readonly scope: SettingScope;
691
+ readonly config?: boolean;
692
+ readonly type: unknown;
693
+ readonly default: T;
694
+ readonly choices?: Readonly<Record<string, string>>;
695
+ readonly range?: {
696
+ readonly min: number;
697
+ readonly max: number;
698
+ readonly step?: number;
699
+ };
700
+ readonly onChange?: (value: T) => void;
701
+ }
702
+ interface GameSettingsApi {
703
+ register<T>(namespace: string, key: string, config: SettingConfig<T>): void;
704
+ get<T = unknown>(namespace: string, key: string): T;
705
+ set<T>(namespace: string, key: string, value: T): Promise<T>;
706
+ }
707
+ interface GameApi {
708
+ readonly settings: GameSettingsApi;
709
+ readonly user?: {
710
+ readonly isGM: boolean;
711
+ };
712
+ }
713
+ type ConfigCollection<T = unknown> = Record<string, T>;
714
+ interface ActorConfig {
715
+ documentClass?: unknown;
716
+ dataModels: ConfigCollection;
717
+ }
718
+ interface ItemConfig {
719
+ documentClass?: unknown;
720
+ dataModels: ConfigCollection;
721
+ }
722
+ interface CombatConfig {
723
+ initiative?: {
724
+ formula: string;
725
+ decimals?: number;
726
+ };
727
+ }
728
+ interface ActiveEffectConfig {
729
+ legacyTransferral?: boolean;
730
+ }
731
+ interface FoundryConfig {
732
+ Actor: ActorConfig;
733
+ Item: ItemConfig;
734
+ Combat: CombatConfig;
735
+ ActiveEffect: ActiveEffectConfig;
736
+ statusEffects?: unknown[];
737
+ [key: string]: unknown;
738
+ }
739
+ //#endregion
740
+ //#region src/migrations/types.d.ts
741
+ interface Migration {
742
+ /** Semver version this migration brings the world to. */
743
+ readonly version: string;
744
+ /** Optional human-readable description — logged when the migration runs and shown in error messages. */
745
+ readonly description?: string;
746
+ /** The migration body. May be sync or async. Should be idempotent (safe to re-run after partial failure). */
747
+ readonly fn: () => void | Promise<void>;
748
+ }
749
+ interface MigrationLogger {
750
+ info(message: string): void;
751
+ warn(message: string): void;
752
+ error(message: string): void;
753
+ }
754
+ interface MigrationRunnerOptions {
755
+ /** System id — used as the `game.settings` namespace. */
756
+ readonly systemId: string;
757
+ /** Migrations in ascending version order. Empty array is allowed (`run()` is a no-op then). */
758
+ readonly migrations: ReadonlyArray<Migration>;
759
+ /** Settings key under `systemId`. Defaults to `'schemaVersion'`. */
760
+ readonly settingKey?: string;
761
+ /**
762
+ * Compatibility floor — worlds with a stored schemaVersion strictly older than this
763
+ * throw `VttfError VTTF-0005` instead of running migrations. Mirrors the
764
+ * `flags.<systemId>.compatibleMigrationVersion` declaration in `system.json`.
765
+ */
766
+ readonly compatibleVersion?: string;
767
+ /**
768
+ * Override the semver comparator. Defaults to `foundry.utils.isNewerVersion`
769
+ * resolved at call time. Test-only injection.
770
+ */
771
+ readonly isNewerVersion?: (next: string, current: string) => boolean;
772
+ /**
773
+ * Override `game.settings`. Defaults to `globalThis.game.settings` resolved
774
+ * at call time. Test-only injection.
775
+ */
776
+ readonly settings?: GameSettingsApi;
777
+ /**
778
+ * Override the logger. Defaults to a `console` + `ui.notifications` adapter.
779
+ * Test-only injection.
780
+ */
781
+ readonly logger?: MigrationLogger;
782
+ }
783
+ interface MigrationRunner {
784
+ /** The target version (last migration's `version`, or `'0.0.0'` if the list is empty). */
785
+ readonly targetVersion: string;
786
+ /**
787
+ * Register the `schemaVersion` setting. Call once from your `init` hook so
788
+ * Foundry knows about it before any world load. Re-calls are idempotent (the
789
+ * underlying `game.settings.register` enforces that).
790
+ */
791
+ register(): void;
792
+ /**
793
+ * Run every migration whose version is newer than the stored
794
+ * `schemaVersion`, in order. Returns the list of versions actually executed
795
+ * (empty when the world is already up to date). Throws `VttfError VTTF-0004`
796
+ * wrapping the original error if any migration throws; throws
797
+ * `VttfError VTTF-0005` if the stored version is older than `compatibleVersion`.
798
+ *
799
+ * Call from your `ready` hook, gated by `game.user.isGM` — this method does
800
+ * NOT enforce GM-only itself so consumers can compose differently if needed.
801
+ */
802
+ run(): Promise<ReadonlyArray<string>>;
803
+ }
804
+ //#endregion
805
+ //#region src/migrations/runner.d.ts
806
+ /**
807
+ * Build a migration runner for a system. See module header for the failure
808
+ * semantics; see `Migration` JSDoc for the per-entry shape.
809
+ *
810
+ * @example
811
+ * ```ts
812
+ * const migrations = createMigrationRunner({
813
+ * systemId: 'my-system',
814
+ * migrations: [
815
+ * { version: '1.0.0', description: 'Rename bio → biography', fn: migrateV1 },
816
+ * { version: '2.0.0', description: 'Add hp.temp', fn: migrateV2 },
817
+ * ],
818
+ * compatibleVersion: '0.9.0',
819
+ * });
820
+ *
821
+ * registerSystem({
822
+ * id: 'my-system',
823
+ * onAfterInit: () => migrations.register(),
824
+ * onReady: async () => {
825
+ * if (!game.user.isGM) return;
826
+ * await migrations.run();
827
+ * },
828
+ * });
829
+ * ```
830
+ */
831
+ declare function createMigrationRunner(options: MigrationRunnerOptions): MigrationRunner;
832
+ //#endregion
833
+ //#region src/register-module.d.ts
834
+ /**
835
+ * registerModule — the module counterpart to `registerSystem`.
836
+ *
837
+ * A module is a guest in someone else's world, and Foundry enforces that. The
838
+ * two differences that matter:
839
+ *
840
+ * - **Sub-type keys are namespaced.** A system registers `character`; a module
841
+ * registering the same thing must register `<module-id>.character`, and the
842
+ * manifest must declare it under `documentTypes`. Forget the prefix and the
843
+ * type silently never appears. This function adds it for you.
844
+ * - **A module never owns the globals.** Document classes, the initiative
845
+ * formula and the status-effect array belong to the system. So there is no
846
+ * option here to replace them — `statusEffects` only appends, which is what
847
+ * a module is allowed to do.
848
+ */
849
+ interface ModuleRegistration {
850
+ /** Module id — must match the folder name and `module.json` `id`. */
851
+ readonly id: string;
852
+ /**
853
+ * Actor sub-types this module contributes, keyed by the bare type name.
854
+ * Registered under `<id>.<type>`, so declare them the same way in
855
+ * `documentTypes.Actor` in your manifest.
856
+ */
857
+ readonly actorDataModels?: Readonly<Record<string, unknown>>;
858
+ /** Item sub-types, same rule as `actorDataModels`. */
859
+ readonly itemDataModels?: Readonly<Record<string, unknown>>;
860
+ /**
861
+ * Status effects to append to `CONFIG.statusEffects`.
862
+ *
863
+ * Appended, never assigned: the array belongs to the system, and replacing
864
+ * it would delete conditions the world depends on.
865
+ */
866
+ readonly statusEffects?: readonly unknown[];
867
+ /** Runs before any CONFIG mutation — the usual home for the module API. */
868
+ readonly onBeforeInit?: () => void;
869
+ /** Runs after the mutations above, inside the same `init` hook. */
870
+ readonly onAfterInit?: () => void;
871
+ /**
872
+ * Runs once on `ready`.
873
+ *
874
+ * **Not GM-gated.** Guard inside your callback when the work is GM-only.
875
+ */
876
+ readonly onReady?: () => void | Promise<void>;
877
+ }
878
+ /**
879
+ * The key Foundry files a module's document sub-type under.
880
+ *
881
+ * Use it wherever you name the type outside `registerModule` — registering the
882
+ * sheet, checking `actor.type`, writing `documentTypes` in the manifest. The
883
+ * prefix is easy to get wrong by hand and fails silently when you do.
884
+ *
885
+ * @example
886
+ * ```ts
887
+ * moduleSubType('pdf-character-sheet', 'pdf'); // 'pdf-character-sheet.pdf'
888
+ * ```
889
+ */
890
+ declare function moduleSubType(moduleId: string, type: string): string;
891
+ /**
892
+ * Register a Foundry module with VTTForge.
893
+ *
894
+ * Calling twice with the same `id` throws VTTF-0001 — almost always a
895
+ * hot-reload artefact or a duplicate import. The CONFIG mutations are deferred
896
+ * until Foundry's `init` hook fires.
897
+ */
898
+ declare function registerModule(config: ModuleRegistration): ModuleRegistration;
899
+ //#endregion
900
+ //#region src/register-system.d.ts
901
+ interface SystemRegistration {
902
+ /** System id — must match the folder name and `system.json` `id`. */
903
+ readonly id: string;
904
+ /** Map of `documentTypes.Actor` key → TypeDataModel class. */
905
+ readonly actorDataModels?: Readonly<Record<string, unknown>>;
906
+ /** Map of `documentTypes.Item` key → TypeDataModel class. */
907
+ readonly itemDataModels?: Readonly<Record<string, unknown>>;
908
+ /** Replacement for `CONFIG.Actor.documentClass`. */
909
+ readonly actorDocumentClass?: unknown;
910
+ /** Replacement for `CONFIG.Item.documentClass`. */
911
+ readonly itemDocumentClass?: unknown;
912
+ /** Global initiative formula — assigned to `CONFIG.Combat.initiative`. */
913
+ readonly combat?: CombatConfig;
914
+ /** Disables legacy Active Effect transferral. Defaults to true. */
915
+ readonly activeEffect?: ActiveEffectConfig;
916
+ /**
917
+ * Replaces `CONFIG.statusEffects` (systems own this array — modules push).
918
+ * If omitted, the existing array is kept untouched.
919
+ */
920
+ readonly statusEffects?: readonly unknown[];
921
+ /**
922
+ * Optional pre-init hook for work that has to run before any of the CONFIG
923
+ * mutations (rare — usually used to assign `globalThis.<systemId>` API).
924
+ */
925
+ readonly onBeforeInit?: () => void;
926
+ /** Optional post-init hook for work that depends on the mutations above. */
927
+ readonly onAfterInit?: () => void;
928
+ /**
929
+ * Optional `ready` hook — fires once after Foundry has finished bootstrap.
930
+ * The natural home for migration runners (`createMigrationRunner().run()`).
931
+ *
932
+ * **Not GM-gated.** Guard inside your callback (`if (!game.user.isGM) return;`)
933
+ * when the work is GM-only — migrations always are.
934
+ */
935
+ readonly onReady?: () => void | Promise<void>;
936
+ }
937
+ /**
938
+ * Register a Foundry system with VTTForge. Idempotency: the same `id` calling
939
+ * twice throws VTTF-0001 — almost always a hot-reload or duplicate import bug.
940
+ *
941
+ * Returns the registration object so consumers can inspect what was applied
942
+ * (useful in tests). The actual CONFIG mutations are deferred until Foundry's
943
+ * `init` hook fires.
944
+ */
945
+ declare function registerSystem(config: SystemRegistration): SystemRegistration;
946
+ //#endregion
947
+ //#region src/system-config.d.ts
948
+ declare class SystemConfig {
949
+ #private;
950
+ readonly systemId: string;
951
+ constructor(systemId: string);
952
+ register<T>(key: string, config: SettingConfig<T>): void;
953
+ get<T>(key: string): T;
954
+ set<T>(key: string, value: T): Promise<T>;
955
+ isRegistered(key: string): boolean;
956
+ }
957
+ //#endregion
958
+ //#region src/index.d.ts
959
+ /**
960
+ * @vttforge/core — runtime utilities for FoundryVTT v13+ systems and modules.
961
+ *
962
+ * v0.1 surface:
963
+ *
964
+ * - registerSystem() — one-call init, replaces Hooks.once("init")
965
+ * - registerModule() — the same for modules, with namespaced sub-types
966
+ * - SystemConfig — typed wrapper around game.settings
967
+ * - BaseTypeDataModel() — TypeDataModel with safe migrateData default
968
+ * - BaseActorSheet() — ActorSheetV2 + HandlebarsApplicationMixin
969
+ * - BaseItemSheet() — ItemSheetV2 + HandlebarsApplicationMixin
970
+ * - fields() — typed bag of foundry.data.fields constructors
971
+ * - InferSchema<T> — derive `system` shape from defineSchema()
972
+ * - createMigrationRunner() — declarative schema migrations (register + run)
973
+ * - VttfError + error registry — VTTF-NNNN codes with docs URLs
974
+ *
975
+ * Foundry classes are resolved from `globalThis.foundry` lazily so the package
976
+ * imports cleanly in Node/tests; concrete Foundry typing arrives with
977
+ * `@vttforge/types` in v1.0.
978
+ */
979
+ declare const VTTFORGE_CORE_VERSION = "0.2.0";
980
+ //#endregion
981
+ export { type ActiveEffectConfig, type ArrayFieldCtor, type ArrayFieldInstance, type ArrayFieldOptions, BaseActorSheet, BaseItemSheet, BaseTypeDataModel, type BooleanFieldCtor, type BooleanFieldInstance, type BooleanFieldOptions, type ColorFieldCtor, type ColorFieldInstance, type ColorFieldOptions, type CombatConfig, type DataFieldOptions, type DocumentClass, type DragDropConfig, ERROR_MANIFEST_VERSION, type ErrorManifest, type FieldInstance, type FieldsApi, type FilePathFieldCtor, type FilePathFieldInstance, type FilePathFieldOptions, type ForeignDocumentFieldCtor, type ForeignDocumentFieldInstance, type ForeignDocumentFieldOptions, type FoundryConfig, type GameApi, type GameSettingsApi, type HTMLFieldCtor, type HTMLFieldInstance, type HTMLFieldOptions, type HookCallback, type HooksApi, type InferField, type InferSchema, type Migration, type MigrationLogger, type MigrationRunner, type MigrationRunnerOptions, type ModuleRegistration, type NumberFieldCtor, type NumberFieldInstance, type NumberFieldOptions, type Prettify, type SchemaFieldCtor, type SchemaFieldInstance, type SchemaFieldOptions, type SetFieldCtor, type SetFieldInstance, type SetFieldOptions, type SettingConfig, type SettingScope, type SheetBaseCtor, type SheetBaseStatics, type StringFieldCtor, type StringFieldInstance, type StringFieldOptions, SystemConfig, type SystemRegistration, type TypeDataModelHooks, type TypedTypeDataModel, type TypedTypeDataModelCtor, VTTFORGE_CORE_VERSION, VTTFORGE_SHEET_CLASS, VttfError, type VttfErrorCode, type VttfErrorEntry, createMigrationRunner, docsUrlFor, fields, getErrorEntry, getErrorManifest, listErrorEntries, moduleSubType, registerModule, registerSystem };
982
+ //# sourceMappingURL=index.d.mts.map