@vttforge/core 0.0.0 → 0.7.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.
- package/CHANGELOG.md +308 -0
- package/LICENSE +21 -0
- package/README.md +9 -6
- package/dist/errors-manifest.json +32 -0
- package/dist/index.d.mts +1049 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +911 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -9
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
//#region src/errors/registry.ts
|
|
2
|
+
const DOCS_BASE_URL = "https://vttforge.dev/errors";
|
|
3
|
+
const REGISTRY = Object.freeze({
|
|
4
|
+
"VTTF-0001": Object.freeze({
|
|
5
|
+
code: "VTTF-0001",
|
|
6
|
+
name: "SystemAlreadyRegistered",
|
|
7
|
+
summary: "registerSystem() was called more than once for the same system id. This is almost always a hot-reload artefact or a duplicate import."
|
|
8
|
+
}),
|
|
9
|
+
"VTTF-0002": Object.freeze({
|
|
10
|
+
code: "VTTF-0002",
|
|
11
|
+
name: "MissingFoundryGlobals",
|
|
12
|
+
summary: "VTTForge code ran in an environment without Foundry globals (game, Hooks, CONFIG). Initialise inside the Foundry runtime, not in a Node test without mocks."
|
|
13
|
+
}),
|
|
14
|
+
"VTTF-0003": Object.freeze({
|
|
15
|
+
code: "VTTF-0003",
|
|
16
|
+
name: "UnknownSetting",
|
|
17
|
+
summary: "SystemConfig.get() / set() was called with a key that was never passed to SystemConfig.register(). Register the setting in your init hook before reading it."
|
|
18
|
+
}),
|
|
19
|
+
"VTTF-0004": Object.freeze({
|
|
20
|
+
code: "VTTF-0004",
|
|
21
|
+
name: "MigrationFailed",
|
|
22
|
+
summary: "A migration function passed to createMigrationRunner() threw. The original error is available on .cause. The schemaVersion setting is not advanced past the failed migration so retrying on the next world load picks up where the failure left off."
|
|
23
|
+
}),
|
|
24
|
+
"VTTF-0005": Object.freeze({
|
|
25
|
+
code: "VTTF-0005",
|
|
26
|
+
name: "WorldTooOldForMigration",
|
|
27
|
+
summary: "createMigrationRunner() was called on a world whose stored schemaVersion is older than the configured compatibleVersion floor. Upgrade the world to a supported intermediate version before continuing — running migrations across the gap would corrupt data."
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Look up a registered entry by code. Throws if the code is unknown — the
|
|
32
|
+
* registry is the source of truth, so missing codes mean a typo.
|
|
33
|
+
*/
|
|
34
|
+
function getErrorEntry(code) {
|
|
35
|
+
const entry = REGISTRY[code];
|
|
36
|
+
if (entry === void 0) throw new Error(`Unknown VTTForge error code: ${code}. Add it to the registry.`);
|
|
37
|
+
return entry;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Return every entry currently in the registry. Used by codegen to emit the
|
|
41
|
+
* runtime constants and the JSON manifest that powers the docs pages.
|
|
42
|
+
*/
|
|
43
|
+
function listErrorEntries() {
|
|
44
|
+
return Object.values(REGISTRY);
|
|
45
|
+
}
|
|
46
|
+
function docsUrlFor(code) {
|
|
47
|
+
return `${DOCS_BASE_URL}/${code}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* VttfError — every error VTTForge throws extends this.
|
|
51
|
+
*
|
|
52
|
+
* - `code` is the registry key (string-narrowed).
|
|
53
|
+
* - `name` is the PascalCase name from the registry — shows up in stack traces.
|
|
54
|
+
* - `docsUrl` points at the docs page.
|
|
55
|
+
* - `cause` uses the native ES2022 mechanism. Multiple causes => pass an
|
|
56
|
+
* `AggregateError` as the cause.
|
|
57
|
+
*/
|
|
58
|
+
var VttfError = class extends Error {
|
|
59
|
+
code;
|
|
60
|
+
docsUrl;
|
|
61
|
+
constructor(code, message, options) {
|
|
62
|
+
const entry = getErrorEntry(code);
|
|
63
|
+
const finalMessage = `[${code}] ${message ?? entry.summary}`;
|
|
64
|
+
super(finalMessage, options);
|
|
65
|
+
this.code = code;
|
|
66
|
+
this.name = entry.name;
|
|
67
|
+
this.docsUrl = docsUrlFor(code);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/base-actor-sheet.ts
|
|
72
|
+
/**
|
|
73
|
+
* BaseActorSheet — `ActorSheetV2 + HandlebarsApplicationMixin` baseline with the
|
|
74
|
+
* boilerplate every shipping system copy-pastes hoisted into the SDK.
|
|
75
|
+
*
|
|
76
|
+
* What this adds beyond stock Foundry v13:
|
|
77
|
+
*
|
|
78
|
+
* - **`static DRAG_DROP`** — declare drag sources / drop targets as data, get
|
|
79
|
+
* `foundry.applications.ux.DragDrop` instances wired in `_onRender` with
|
|
80
|
+
* `isEditable`-gated permissions and a sensible default `_onDragStart` that
|
|
81
|
+
* serialises `data-item-id` elements as `{ type: "Item", uuid }`.
|
|
82
|
+
* - **`_prepareContext` auto-fills `context.tabs[group]`** for every group
|
|
83
|
+
* declared in ApplicationV2's `static TABS`, so subclass `_prepareContext`
|
|
84
|
+
* implementations stop having to call `_prepareTabs(group)` by hand.
|
|
85
|
+
* - **Typed drop dispatch** — override `onDropItem(item, event)` /
|
|
86
|
+
* `onDropActor(actor, event)` / `onDropFolder(folder, event)` /
|
|
87
|
+
* `onDropActiveEffect(effect, event)` and skip the `fromUuid()` ceremony.
|
|
88
|
+
* Returning `undefined` falls through to Foundry's default `_onDropX`
|
|
89
|
+
* behaviour; return any other value to take ownership.
|
|
90
|
+
*
|
|
91
|
+
* Intentional non-additions:
|
|
92
|
+
*
|
|
93
|
+
* - `editImage` action — already shipped by `DocumentSheetV2` (inherited by
|
|
94
|
+
* `ActorSheetV2`). Templates wire `<img data-edit="img">` and Foundry's
|
|
95
|
+
* built-in action handles the `FilePicker` flow.
|
|
96
|
+
* - `_getTabs()` — ApplicationV2 already owns the tab state machine; we only
|
|
97
|
+
* eliminate the `_prepareTabs` call in `_prepareContext`.
|
|
98
|
+
*
|
|
99
|
+
* Resolved lazily so subclasses can be declared at module load without
|
|
100
|
+
* Foundry globals existing yet (test boot, ESM hoist).
|
|
101
|
+
*/
|
|
102
|
+
function resolveBases$1() {
|
|
103
|
+
const foundry = globalThis.foundry;
|
|
104
|
+
const Base = foundry?.applications?.sheets?.ActorSheetV2;
|
|
105
|
+
const mixin = foundry?.applications?.api?.HandlebarsApplicationMixin;
|
|
106
|
+
if (typeof Base !== "function" || typeof mixin !== "function") throw new VttfError("VTTF-0002", "foundry.applications.sheets.ActorSheetV2 and/or foundry.applications.api.HandlebarsApplicationMixin are not available. Define your BaseActorSheet subclasses inside the Foundry runtime (or stub the global in tests).");
|
|
107
|
+
return {
|
|
108
|
+
Base,
|
|
109
|
+
mixin
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function resolveDragDrop$1() {
|
|
113
|
+
const ctor = globalThis.foundry?.applications?.ux?.DragDrop;
|
|
114
|
+
return typeof ctor === "function" ? ctor : void 0;
|
|
115
|
+
}
|
|
116
|
+
async function resolveFromUuid(uuid) {
|
|
117
|
+
const fn = globalThis.fromUuid;
|
|
118
|
+
if (typeof fn !== "function") return null;
|
|
119
|
+
return fn(uuid);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Marker class that consumer CSS uses for scoping. Always present on every
|
|
123
|
+
* VTTForge-derived sheet so rules like `.vttforge .actor-sheet { ... }` work.
|
|
124
|
+
*/
|
|
125
|
+
const VTTFORGE_SHEET_CLASS = "vttforge";
|
|
126
|
+
/**
|
|
127
|
+
* Build the `BaseActorSheet` for the current Foundry runtime. See module
|
|
128
|
+
* header for the boilerplate this base eliminates.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* class CharacterSheet extends BaseActorSheet() {
|
|
133
|
+
* static DEFAULT_OPTIONS = foundry.utils.mergeObject(
|
|
134
|
+
* super.DEFAULT_OPTIONS,
|
|
135
|
+
* { classes: ['my-system'], position: { width: 720 } },
|
|
136
|
+
* );
|
|
137
|
+
* static PARTS = { ... };
|
|
138
|
+
* static TABS = {
|
|
139
|
+
* primary: {
|
|
140
|
+
* tabs: [
|
|
141
|
+
* { id: 'features', group: 'primary', label: 'Features' },
|
|
142
|
+
* { id: 'inventory', group: 'primary', label: 'Inventory' },
|
|
143
|
+
* ],
|
|
144
|
+
* initial: 'features',
|
|
145
|
+
* },
|
|
146
|
+
* };
|
|
147
|
+
* static DRAG_DROP = [{ dragSelector: '.item[draggable=true]', dropSelector: null }];
|
|
148
|
+
* async onDropItem(item, event) {
|
|
149
|
+
* if (item.type !== 'weapon') return false;
|
|
150
|
+
* // …fall through to super by returning undefined.
|
|
151
|
+
* }
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
function BaseActorSheet() {
|
|
156
|
+
const { Base, mixin } = resolveBases$1();
|
|
157
|
+
const Mixed = mixin(Base);
|
|
158
|
+
class VttforgeBaseActorSheet extends Mixed {
|
|
159
|
+
static DEFAULT_OPTIONS = {
|
|
160
|
+
classes: [VTTFORGE_SHEET_CLASS],
|
|
161
|
+
window: { resizable: true },
|
|
162
|
+
position: {
|
|
163
|
+
width: 600,
|
|
164
|
+
height: 700
|
|
165
|
+
},
|
|
166
|
+
tag: "form",
|
|
167
|
+
form: {
|
|
168
|
+
submitOnChange: true,
|
|
169
|
+
closeOnSubmit: false
|
|
170
|
+
},
|
|
171
|
+
actions: { vttforgeTab: VttforgeBaseActorSheet._onTab }
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Declarative DragDrop entries. Each becomes a
|
|
175
|
+
* `foundry.applications.ux.DragDrop` instance bound in `_onRender`.
|
|
176
|
+
* Subclasses override by re-declaring `static DRAG_DROP = [...]`.
|
|
177
|
+
*/
|
|
178
|
+
static DRAG_DROP = [];
|
|
179
|
+
/**
|
|
180
|
+
* Augment ApplicationV2's context with `tabs[group]` for sheets that
|
|
181
|
+
* declare **multiple** `static TABS` groups. ApplicationV2 already
|
|
182
|
+
* auto-populates `context.tabs` (keyed by tab id) for single-group
|
|
183
|
+
* sheets — overriding that flat shape would force every consumer to
|
|
184
|
+
* either unwrap or write `context.tabs.<group>.<tabId>` in templates.
|
|
185
|
+
* Multi-group sheets get nested `context.tabs.<group>.<tabId>` because
|
|
186
|
+
* ApplicationV2 returns `{}` for them by default.
|
|
187
|
+
*/
|
|
188
|
+
async _prepareContext(options) {
|
|
189
|
+
const superPrepare = Mixed.prototype._prepareContext;
|
|
190
|
+
const context = typeof superPrepare === "function" ? await superPrepare.call(this, options) : {};
|
|
191
|
+
const tabsConfig = this.constructor.TABS;
|
|
192
|
+
if (tabsConfig && typeof tabsConfig === "object") {
|
|
193
|
+
const groups = Object.keys(tabsConfig);
|
|
194
|
+
if (groups.length > 1) {
|
|
195
|
+
const prepareTabs = this._prepareTabs;
|
|
196
|
+
const tabs = {};
|
|
197
|
+
for (const group of groups) tabs[group] = typeof prepareTabs === "function" ? prepareTabs.call(this, group) : {};
|
|
198
|
+
context.tabs = tabs;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return context;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Default `tab` action handler. ApplicationV2 doesn't ship one, so every
|
|
205
|
+
* sheet that uses `<button data-action="tab" data-tab=… data-group=…>`
|
|
206
|
+
* has to wire its own. We toggle the `.active` class on the matching
|
|
207
|
+
* nav element (`[data-action="tab"][data-tab=…][data-group=…]`) and
|
|
208
|
+
* on `section.tab[data-tab=…][data-group=…]`, then update
|
|
209
|
+
* `sheet.tabGroups[group]` so subsequent re-renders pick the right
|
|
210
|
+
* initial tab.
|
|
211
|
+
*
|
|
212
|
+
* ApplicationV2's action dispatcher binds `this` to the sheet instance
|
|
213
|
+
* at call time even though the handler is declared `static`.
|
|
214
|
+
*/
|
|
215
|
+
static _onTab(_event, target) {
|
|
216
|
+
const sheet = this;
|
|
217
|
+
const group = target.dataset?.group;
|
|
218
|
+
const tab = target.dataset?.tab;
|
|
219
|
+
if (!group || !tab) return;
|
|
220
|
+
sheet.tabGroups[group] = tab;
|
|
221
|
+
const root = sheet.element;
|
|
222
|
+
if (!root) return;
|
|
223
|
+
for (const link of root.querySelectorAll(`[data-action="vttforgeTab"][data-group="${group}"]`)) link.classList.toggle("active", link.dataset.tab === tab);
|
|
224
|
+
for (const section of root.querySelectorAll(`section.tab[data-group="${group}"]`)) section.classList.toggle("active", section.dataset.tab === tab);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Wire each `static DRAG_DROP` entry into a real `DragDrop` instance.
|
|
228
|
+
* Permissions default to `this.isEditable`; callbacks default to
|
|
229
|
+
* `_onDragStart` / `_onDrop`. Subclasses extending `_onRender` MUST call
|
|
230
|
+
* `super._onRender(context, options)` to keep DragDrop wired.
|
|
231
|
+
*/
|
|
232
|
+
_onRender(context, options) {
|
|
233
|
+
const superRender = Mixed.prototype._onRender;
|
|
234
|
+
if (typeof superRender === "function") superRender.call(this, context, options);
|
|
235
|
+
const configs = this.constructor.DRAG_DROP;
|
|
236
|
+
if (!configs?.length) return;
|
|
237
|
+
const DragDrop = resolveDragDrop$1();
|
|
238
|
+
if (!DragDrop) return;
|
|
239
|
+
const element = this.element;
|
|
240
|
+
if (!element) return;
|
|
241
|
+
const onDragStart = this._onDragStart;
|
|
242
|
+
const onDrop = this._onDrop;
|
|
243
|
+
for (const cfg of configs) new DragDrop({
|
|
244
|
+
dragSelector: cfg.dragSelector,
|
|
245
|
+
dropSelector: cfg.dropSelector,
|
|
246
|
+
permissions: {
|
|
247
|
+
dragstart: cfg.permissions?.dragstart ?? (() => this.#isEditable()),
|
|
248
|
+
drop: cfg.permissions?.drop ?? (() => this.#isEditable())
|
|
249
|
+
},
|
|
250
|
+
callbacks: {
|
|
251
|
+
...typeof onDragStart === "function" ? { dragstart: onDragStart.bind(this) } : {},
|
|
252
|
+
...typeof onDrop === "function" ? { drop: onDrop.bind(this) } : {},
|
|
253
|
+
...cfg.callbacks ?? {}
|
|
254
|
+
}
|
|
255
|
+
}).bind(element);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Default drag handler — serialises the item identified by
|
|
259
|
+
* `data-item-id` on the drag source element. Override for richer payloads
|
|
260
|
+
* (Actor drags, custom UUIDs).
|
|
261
|
+
*/
|
|
262
|
+
_onDragStart(event) {
|
|
263
|
+
const itemId = event.currentTarget?.dataset?.itemId;
|
|
264
|
+
if (!itemId || !event.dataTransfer) return;
|
|
265
|
+
const item = (this.document?.items)?.get(itemId);
|
|
266
|
+
if (!item) return;
|
|
267
|
+
event.dataTransfer.setData("application/json", JSON.stringify({
|
|
268
|
+
type: "Item",
|
|
269
|
+
uuid: item.uuid
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Typed drop sugar. Subclasses override this instead of `_onDropItem`
|
|
274
|
+
* to skip the `fromUuid()` ceremony. Return `undefined` to fall through
|
|
275
|
+
* to Foundry's default `_onDropItem`; return anything else to take
|
|
276
|
+
* ownership of the drop.
|
|
277
|
+
*/
|
|
278
|
+
async onDropItem(_item, _event) {}
|
|
279
|
+
async onDropActor(_actor, _event) {}
|
|
280
|
+
async onDropFolder(_folder, _event) {}
|
|
281
|
+
async onDropActiveEffect(_effect, _event) {}
|
|
282
|
+
async _onDropItem(event, data) {
|
|
283
|
+
return this.#dispatchDrop("_onDropItem", "onDropItem", event, data);
|
|
284
|
+
}
|
|
285
|
+
async _onDropActor(event, data) {
|
|
286
|
+
return this.#dispatchDrop("_onDropActor", "onDropActor", event, data);
|
|
287
|
+
}
|
|
288
|
+
async _onDropFolder(event, data) {
|
|
289
|
+
return this.#dispatchDrop("_onDropFolder", "onDropFolder", event, data);
|
|
290
|
+
}
|
|
291
|
+
async _onDropActiveEffect(event, data) {
|
|
292
|
+
return this.#dispatchDrop("_onDropActiveEffect", "onDropActiveEffect", event, data);
|
|
293
|
+
}
|
|
294
|
+
async #dispatchDrop(superKey, sugarKey, event, data) {
|
|
295
|
+
const uuid = data?.uuid;
|
|
296
|
+
if (uuid) {
|
|
297
|
+
const doc = await resolveFromUuid(uuid);
|
|
298
|
+
if (doc) {
|
|
299
|
+
const result = await this[sugarKey](doc, event);
|
|
300
|
+
if (result !== void 0) return result;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const superFn = Mixed.prototype[superKey];
|
|
304
|
+
if (typeof superFn === "function") return superFn.call(this, event, data);
|
|
305
|
+
}
|
|
306
|
+
#isEditable() {
|
|
307
|
+
return Boolean(this.isEditable);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return VttforgeBaseActorSheet;
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/base-item-sheet.ts
|
|
314
|
+
function resolveBases() {
|
|
315
|
+
const foundry = globalThis.foundry;
|
|
316
|
+
const Base = foundry?.applications?.sheets?.ItemSheetV2;
|
|
317
|
+
const mixin = foundry?.applications?.api?.HandlebarsApplicationMixin;
|
|
318
|
+
if (typeof Base !== "function" || typeof mixin !== "function") throw new VttfError("VTTF-0002", "foundry.applications.sheets.ItemSheetV2 and/or foundry.applications.api.HandlebarsApplicationMixin are not available. Define your BaseItemSheet subclasses inside the Foundry runtime (or stub the global in tests).");
|
|
319
|
+
return {
|
|
320
|
+
Base,
|
|
321
|
+
mixin
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function resolveDragDrop() {
|
|
325
|
+
const ctor = globalThis.foundry?.applications?.ux?.DragDrop;
|
|
326
|
+
return typeof ctor === "function" ? ctor : void 0;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Build the `BaseItemSheet` for the current Foundry runtime.
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```ts
|
|
333
|
+
* class WeaponSheet extends BaseItemSheet() {
|
|
334
|
+
* static DEFAULT_OPTIONS = foundry.utils.mergeObject(
|
|
335
|
+
* super.DEFAULT_OPTIONS,
|
|
336
|
+
* { classes: ['my-system'], position: { width: 540 } },
|
|
337
|
+
* );
|
|
338
|
+
* static PARTS = { ... };
|
|
339
|
+
* static TABS = {
|
|
340
|
+
* primary: {
|
|
341
|
+
* tabs: [
|
|
342
|
+
* { id: 'description', group: 'primary', label: 'Description' },
|
|
343
|
+
* { id: 'details', group: 'primary', label: 'Details' },
|
|
344
|
+
* ],
|
|
345
|
+
* initial: 'description',
|
|
346
|
+
* },
|
|
347
|
+
* };
|
|
348
|
+
* }
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
function BaseItemSheet() {
|
|
352
|
+
const { Base, mixin } = resolveBases();
|
|
353
|
+
const Mixed = mixin(Base);
|
|
354
|
+
class VttforgeBaseItemSheet extends Mixed {
|
|
355
|
+
static DEFAULT_OPTIONS = {
|
|
356
|
+
classes: [VTTFORGE_SHEET_CLASS],
|
|
357
|
+
window: { resizable: true },
|
|
358
|
+
position: {
|
|
359
|
+
width: 520,
|
|
360
|
+
height: 480
|
|
361
|
+
},
|
|
362
|
+
tag: "form",
|
|
363
|
+
form: {
|
|
364
|
+
submitOnChange: true,
|
|
365
|
+
closeOnSubmit: false
|
|
366
|
+
},
|
|
367
|
+
actions: { vttforgeTab: VttforgeBaseItemSheet._onTab }
|
|
368
|
+
};
|
|
369
|
+
static DRAG_DROP = [];
|
|
370
|
+
/**
|
|
371
|
+
* Multi-group sheets get nested `context.tabs.<group>.<tabId>` because
|
|
372
|
+
* ApplicationV2 returns `{}` for them by default; single-group sheets
|
|
373
|
+
* use ApplicationV2's flat `context.tabs.<tabId>` shape untouched. See
|
|
374
|
+
* BaseActorSheet for the long version.
|
|
375
|
+
*/
|
|
376
|
+
async _prepareContext(options) {
|
|
377
|
+
const superPrepare = Mixed.prototype._prepareContext;
|
|
378
|
+
const context = typeof superPrepare === "function" ? await superPrepare.call(this, options) : {};
|
|
379
|
+
const tabsConfig = this.constructor.TABS;
|
|
380
|
+
if (tabsConfig && typeof tabsConfig === "object") {
|
|
381
|
+
const groups = Object.keys(tabsConfig);
|
|
382
|
+
if (groups.length > 1) {
|
|
383
|
+
const prepareTabs = this._prepareTabs;
|
|
384
|
+
const tabs = {};
|
|
385
|
+
for (const group of groups) tabs[group] = typeof prepareTabs === "function" ? prepareTabs.call(this, group) : {};
|
|
386
|
+
context.tabs = tabs;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return context;
|
|
390
|
+
}
|
|
391
|
+
static _onTab(_event, target) {
|
|
392
|
+
const sheet = this;
|
|
393
|
+
const group = target.dataset?.group;
|
|
394
|
+
const tab = target.dataset?.tab;
|
|
395
|
+
if (!group || !tab) return;
|
|
396
|
+
sheet.tabGroups[group] = tab;
|
|
397
|
+
const root = sheet.element;
|
|
398
|
+
if (!root) return;
|
|
399
|
+
for (const link of root.querySelectorAll(`[data-action="vttforgeTab"][data-group="${group}"]`)) link.classList.toggle("active", link.dataset.tab === tab);
|
|
400
|
+
for (const section of root.querySelectorAll(`section.tab[data-group="${group}"]`)) section.classList.toggle("active", section.dataset.tab === tab);
|
|
401
|
+
}
|
|
402
|
+
_onRender(context, options) {
|
|
403
|
+
const superRender = Mixed.prototype._onRender;
|
|
404
|
+
if (typeof superRender === "function") superRender.call(this, context, options);
|
|
405
|
+
const configs = this.constructor.DRAG_DROP;
|
|
406
|
+
if (!configs?.length) return;
|
|
407
|
+
const DragDrop = resolveDragDrop();
|
|
408
|
+
if (!DragDrop) return;
|
|
409
|
+
const element = this.element;
|
|
410
|
+
if (!element) return;
|
|
411
|
+
const onDragStart = this._onDragStart;
|
|
412
|
+
const onDrop = this._onDrop;
|
|
413
|
+
for (const cfg of configs) new DragDrop({
|
|
414
|
+
dragSelector: cfg.dragSelector,
|
|
415
|
+
dropSelector: cfg.dropSelector,
|
|
416
|
+
permissions: {
|
|
417
|
+
dragstart: cfg.permissions?.dragstart ?? (() => this.#isEditable()),
|
|
418
|
+
drop: cfg.permissions?.drop ?? (() => this.#isEditable())
|
|
419
|
+
},
|
|
420
|
+
callbacks: {
|
|
421
|
+
...typeof onDragStart === "function" ? { dragstart: onDragStart.bind(this) } : {},
|
|
422
|
+
...typeof onDrop === "function" ? { drop: onDrop.bind(this) } : {},
|
|
423
|
+
...cfg.callbacks ?? {}
|
|
424
|
+
}
|
|
425
|
+
}).bind(element);
|
|
426
|
+
}
|
|
427
|
+
#isEditable() {
|
|
428
|
+
return Boolean(this.isEditable);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return VttforgeBaseItemSheet;
|
|
432
|
+
}
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/base-type-data-model.ts
|
|
435
|
+
function resolveTypeDataModelClass() {
|
|
436
|
+
const cls = globalThis.foundry?.abstract?.TypeDataModel;
|
|
437
|
+
if (typeof cls !== "function") throw new VttfError("VTTF-0002", "foundry.abstract.TypeDataModel is not available. Define your BaseTypeDataModel subclasses inside the Foundry runtime (or stub the global in tests).");
|
|
438
|
+
return cls;
|
|
439
|
+
}
|
|
440
|
+
function BaseTypeDataModel(defineSchema) {
|
|
441
|
+
const Base = resolveTypeDataModelClass();
|
|
442
|
+
class VttforgeBaseTypeDataModel extends Base {
|
|
443
|
+
/**
|
|
444
|
+
* Default no-op so subclasses can omit it when they have no value-level
|
|
445
|
+
* migrations. Always end with `super.migrateData(data)` if you override.
|
|
446
|
+
*/
|
|
447
|
+
static migrateData(data) {
|
|
448
|
+
const superMigrateData = Base.migrateData;
|
|
449
|
+
if (typeof superMigrateData === "function") return superMigrateData.call(VttforgeBaseTypeDataModel, data);
|
|
450
|
+
return data;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* No-op stub. Override per type to initialize fields whose values Active
|
|
454
|
+
* Effects need to consume — base max HP, base AC, etc. Foundry calls this
|
|
455
|
+
* BEFORE applying Active Effects, so anything you set here is the input
|
|
456
|
+
* that AE changes (`ADD`, `MULTIPLY`, `OVERRIDE`, …) operate on.
|
|
457
|
+
*
|
|
458
|
+
* Use `prepareDerivedData()` instead for values that depend on the
|
|
459
|
+
* AE-mutated state (modifiers, percentages, totals).
|
|
460
|
+
*
|
|
461
|
+
* Never write to the database here — purely in-memory.
|
|
462
|
+
*/
|
|
463
|
+
prepareBaseData() {}
|
|
464
|
+
/**
|
|
465
|
+
* No-op stub. Override per type to compute derived values from the
|
|
466
|
+
* AE-mutated state (modifiers, percentages, totals). Runs AFTER Active
|
|
467
|
+
* Effects apply; use `prepareBaseData()` for values that AEs need to read.
|
|
468
|
+
*
|
|
469
|
+
* Never write to the database here — purely in-memory.
|
|
470
|
+
*/
|
|
471
|
+
prepareDerivedData() {}
|
|
472
|
+
}
|
|
473
|
+
if (defineSchema !== void 0) Object.defineProperty(VttforgeBaseTypeDataModel, "defineSchema", {
|
|
474
|
+
value: defineSchema,
|
|
475
|
+
writable: true,
|
|
476
|
+
configurable: true
|
|
477
|
+
});
|
|
478
|
+
return VttforgeBaseTypeDataModel;
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/data/fields.ts
|
|
482
|
+
/**
|
|
483
|
+
* `fields()` — typed bag of Foundry v13 data-field constructors.
|
|
484
|
+
*
|
|
485
|
+
* Foundry idiom inside `defineSchema()` is `const f = foundry.data.fields`. We
|
|
486
|
+
* mirror that, but routed through a factory so the import succeeds in Node
|
|
487
|
+
* (tests, IDE typecheck) where the global is absent. The factory resolves
|
|
488
|
+
* `globalThis.foundry.data.fields` lazily — same pattern as
|
|
489
|
+
* `BaseTypeDataModel()` (see `base-type-data-model.ts:25`) and
|
|
490
|
+
* `BaseActorSheet()` (see `base-actor-sheet.ts:40`).
|
|
491
|
+
*
|
|
492
|
+
* v0.1 covers eight fields (PRD §7): NumberField, StringField, BooleanField,
|
|
493
|
+
* HTMLField, ArrayField, SchemaField, ColorField, FilePathField. The instance
|
|
494
|
+
* interfaces carry a phantom `[BRAND]` tag and an `options` capture so the
|
|
495
|
+
* conditional types in `./infer-schema.ts` can extract the runtime semantics
|
|
496
|
+
* (e.g. `nullable: true`).
|
|
497
|
+
*
|
|
498
|
+
* `EmbeddedDataField`, `EmbeddedDocumentField`, `TypedSchemaField`, and the
|
|
499
|
+
* full required×initial nullability matrix ship with `@vttforge/types` v1.0.
|
|
500
|
+
*/
|
|
501
|
+
/**
|
|
502
|
+
* Resolve `globalThis.foundry.data.fields` and return it typed as `FieldsApi`.
|
|
503
|
+
*
|
|
504
|
+
* Call this inside `defineSchema()` (or any code that runs after Foundry's
|
|
505
|
+
* `init` hook). Calling at module scope will throw when imported from Node
|
|
506
|
+
* tests — the global only exists inside the Foundry runtime.
|
|
507
|
+
*
|
|
508
|
+
* @throws `VttfError` with code `VTTF-0002` when `foundry.data.fields` is
|
|
509
|
+
* missing.
|
|
510
|
+
*/
|
|
511
|
+
function fields() {
|
|
512
|
+
const f = globalThis.foundry?.data?.fields;
|
|
513
|
+
if (f === void 0 || f === null) throw new VttfError("VTTF-0002", "foundry.data.fields is not available. Call fields() inside the Foundry runtime (or stub the global in tests).");
|
|
514
|
+
return f;
|
|
515
|
+
}
|
|
516
|
+
//#endregion
|
|
517
|
+
//#region src/errors/manifest.ts
|
|
518
|
+
/**
|
|
519
|
+
* Typed runtime view over the VTTF-NNNN registry.
|
|
520
|
+
*
|
|
521
|
+
* Same data as `listErrorEntries()` — the manifest wraps it in a versioned
|
|
522
|
+
* envelope so external tooling (the v0.3 docs site, IDE extensions, lint
|
|
523
|
+
* rules) has a stable shape to consume. The matching JSON projection is
|
|
524
|
+
* emitted to `dist/errors-manifest.json` at build time by
|
|
525
|
+
* `packages/core/scripts/codegen-errors.mjs`.
|
|
526
|
+
*/
|
|
527
|
+
const ERROR_MANIFEST_VERSION = 1;
|
|
528
|
+
/**
|
|
529
|
+
* Snapshot the current registry as a manifest object. Recomputed on every
|
|
530
|
+
* call — cheap (the registry is a frozen literal). For the JSON projection
|
|
531
|
+
* shipped with the package, see `dist/errors-manifest.json`.
|
|
532
|
+
*/
|
|
533
|
+
function getErrorManifest() {
|
|
534
|
+
return {
|
|
535
|
+
version: 1,
|
|
536
|
+
package: "@vttforge/core",
|
|
537
|
+
entries: listErrorEntries()
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/migrations/runner.ts
|
|
542
|
+
/**
|
|
543
|
+
* `createMigrationRunner` — declarative schema migrations for Foundry systems.
|
|
544
|
+
*
|
|
545
|
+
* Replaces the copy-pasted "schemaVersion setting + Hooks.once('ready') +
|
|
546
|
+
* isNewerVersion compare + sequential await" pattern that every system
|
|
547
|
+
* eventually grows on its own. The runner owns no hooks — call
|
|
548
|
+
* `register()` from your `init` hook and `run()` from your `ready` hook
|
|
549
|
+
* (gated by `game.user.isGM`).
|
|
550
|
+
*
|
|
551
|
+
* Versions are semver strings, compared with `foundry.utils.isNewerVersion`.
|
|
552
|
+
* The data lives in a per-system world setting and lines up cleanly with
|
|
553
|
+
* `system.json`'s `flags.<systemId>.needsMigrationVersion` /
|
|
554
|
+
* `compatibleMigrationVersion`.
|
|
555
|
+
*
|
|
556
|
+
* Failures advance `schemaVersion` only past migrations that *completed* — a
|
|
557
|
+
* mid-sequence throw leaves the world at the last successful version so the
|
|
558
|
+
* retry on the next world load picks up exactly where it failed.
|
|
559
|
+
*/
|
|
560
|
+
const DEFAULT_SETTING_KEY = "schemaVersion";
|
|
561
|
+
const INITIAL_VERSION = "0.0.0";
|
|
562
|
+
function resolveIsNewerVersion() {
|
|
563
|
+
const fn = globalThis.foundry?.utils?.isNewerVersion;
|
|
564
|
+
if (typeof fn === "function") return fn;
|
|
565
|
+
return naiveIsNewerVersion;
|
|
566
|
+
}
|
|
567
|
+
function naiveIsNewerVersion(next, current) {
|
|
568
|
+
const parse = (v) => v.split(".").map((part) => {
|
|
569
|
+
const n = Number.parseInt(part, 10);
|
|
570
|
+
return Number.isNaN(n) ? 0 : n;
|
|
571
|
+
});
|
|
572
|
+
const a = parse(next);
|
|
573
|
+
const b = parse(current);
|
|
574
|
+
const len = Math.max(a.length, b.length);
|
|
575
|
+
for (let i = 0; i < len; i++) {
|
|
576
|
+
const ai = a[i] ?? 0;
|
|
577
|
+
const bi = b[i] ?? 0;
|
|
578
|
+
if (ai > bi) return true;
|
|
579
|
+
if (ai < bi) return false;
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
function resolveSettings() {
|
|
584
|
+
const settings = globalThis.game?.settings;
|
|
585
|
+
if (settings === void 0 || typeof settings.register !== "function" || typeof settings.get !== "function" || typeof settings.set !== "function") throw new VttfError("VTTF-0002", "globalThis.game.settings is not available — call createMigrationRunner().register() inside the Foundry runtime (or pass an explicit settings adapter in MigrationRunnerOptions).");
|
|
586
|
+
return settings;
|
|
587
|
+
}
|
|
588
|
+
function resolveLogger() {
|
|
589
|
+
const notifications = globalThis.ui?.notifications;
|
|
590
|
+
return {
|
|
591
|
+
info(message) {
|
|
592
|
+
console.info(message);
|
|
593
|
+
notifications?.info?.(message);
|
|
594
|
+
},
|
|
595
|
+
warn(message) {
|
|
596
|
+
console.warn(message);
|
|
597
|
+
notifications?.warn?.(message);
|
|
598
|
+
},
|
|
599
|
+
error(message) {
|
|
600
|
+
console.error(message);
|
|
601
|
+
notifications?.error?.(message);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function lastVersion(migrations) {
|
|
606
|
+
return migrations.at(-1)?.version ?? INITIAL_VERSION;
|
|
607
|
+
}
|
|
608
|
+
function assertAscending(migrations, isNewer) {
|
|
609
|
+
for (let i = 1; i < migrations.length; i++) {
|
|
610
|
+
const prevMig = migrations[i - 1];
|
|
611
|
+
const nextMig = migrations[i];
|
|
612
|
+
if (prevMig === void 0 || nextMig === void 0) continue;
|
|
613
|
+
if (!isNewer(nextMig.version, prevMig.version)) throw new VttfError("VTTF-0004", `Migration list out of order: ${nextMig.version} must be newer than ${prevMig.version}.`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Build a migration runner for a system. See module header for the failure
|
|
618
|
+
* semantics; see `Migration` JSDoc for the per-entry shape.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```ts
|
|
622
|
+
* const migrations = createMigrationRunner({
|
|
623
|
+
* systemId: 'my-system',
|
|
624
|
+
* migrations: [
|
|
625
|
+
* { version: '1.0.0', description: 'Rename bio → biography', fn: migrateV1 },
|
|
626
|
+
* { version: '2.0.0', description: 'Add hp.temp', fn: migrateV2 },
|
|
627
|
+
* ],
|
|
628
|
+
* compatibleVersion: '0.9.0',
|
|
629
|
+
* });
|
|
630
|
+
*
|
|
631
|
+
* registerSystem({
|
|
632
|
+
* id: 'my-system',
|
|
633
|
+
* onAfterInit: () => migrations.register(),
|
|
634
|
+
* onReady: async () => {
|
|
635
|
+
* if (!game.user.isGM) return;
|
|
636
|
+
* await migrations.run();
|
|
637
|
+
* },
|
|
638
|
+
* });
|
|
639
|
+
* ```
|
|
640
|
+
*/
|
|
641
|
+
function createMigrationRunner(options) {
|
|
642
|
+
const settingKey = options.settingKey ?? DEFAULT_SETTING_KEY;
|
|
643
|
+
const target = lastVersion(options.migrations);
|
|
644
|
+
const settingsOverride = options.settings;
|
|
645
|
+
const loggerOverride = options.logger;
|
|
646
|
+
const isNewerOverride = options.isNewerVersion;
|
|
647
|
+
return {
|
|
648
|
+
targetVersion: target,
|
|
649
|
+
register() {
|
|
650
|
+
(settingsOverride ?? resolveSettings()).register(options.systemId, settingKey, {
|
|
651
|
+
name: "Schema Version",
|
|
652
|
+
hint: "Internal schema version for VTTForge data migration tracking. Do not edit by hand.",
|
|
653
|
+
scope: "world",
|
|
654
|
+
config: false,
|
|
655
|
+
type: String,
|
|
656
|
+
default: INITIAL_VERSION
|
|
657
|
+
});
|
|
658
|
+
},
|
|
659
|
+
async run() {
|
|
660
|
+
if (options.migrations.length === 0) return [];
|
|
661
|
+
const settings = settingsOverride ?? resolveSettings();
|
|
662
|
+
const logger = loggerOverride ?? resolveLogger();
|
|
663
|
+
const isNewer = isNewerOverride ?? resolveIsNewerVersion();
|
|
664
|
+
assertAscending(options.migrations, isNewer);
|
|
665
|
+
const current = settings.get(options.systemId, settingKey) ?? INITIAL_VERSION;
|
|
666
|
+
if (options.compatibleVersion !== void 0) {
|
|
667
|
+
if (isNewer(options.compatibleVersion, current)) throw new VttfError("VTTF-0005", `World schemaVersion ${current} is older than ${options.systemId}'s compatibleVersion ${options.compatibleVersion}. Upgrade through an intermediate release first.`);
|
|
668
|
+
}
|
|
669
|
+
const pending = options.migrations.filter((m) => isNewer(m.version, current));
|
|
670
|
+
if (pending.length === 0) return [];
|
|
671
|
+
const ran = [];
|
|
672
|
+
let lastApplied = current;
|
|
673
|
+
logger.warn(`${options.systemId} | Running ${pending.length} pending migration(s) from ${current} to ${target}.`);
|
|
674
|
+
for (const migration of pending) {
|
|
675
|
+
const label = migration.description ? `${migration.version} — ${migration.description}` : migration.version;
|
|
676
|
+
logger.info(`${options.systemId} | Migrating to ${label}`);
|
|
677
|
+
try {
|
|
678
|
+
await migration.fn();
|
|
679
|
+
} catch (cause) {
|
|
680
|
+
if (isNewer(lastApplied, current)) await settings.set(options.systemId, settingKey, lastApplied);
|
|
681
|
+
throw new VttfError("VTTF-0004", `Migration to ${label} failed for system "${options.systemId}". schemaVersion left at ${lastApplied}.`, { cause });
|
|
682
|
+
}
|
|
683
|
+
await settings.set(options.systemId, settingKey, migration.version);
|
|
684
|
+
lastApplied = migration.version;
|
|
685
|
+
ran.push(migration.version);
|
|
686
|
+
}
|
|
687
|
+
logger.info(`${options.systemId} | Migration complete. schemaVersion = ${target}.`);
|
|
688
|
+
return ran;
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/register-module.ts
|
|
694
|
+
/**
|
|
695
|
+
* registerModule — the module counterpart to `registerSystem`.
|
|
696
|
+
*
|
|
697
|
+
* A module is a guest in someone else's world, and Foundry enforces that. The
|
|
698
|
+
* two differences that matter:
|
|
699
|
+
*
|
|
700
|
+
* - **Sub-type keys are namespaced.** A system registers `character`; a module
|
|
701
|
+
* registering the same thing must register `<module-id>.character`, and the
|
|
702
|
+
* manifest must declare it under `documentTypes`. Forget the prefix and the
|
|
703
|
+
* type silently never appears. This function adds it for you.
|
|
704
|
+
* - **A module never owns the globals.** Document classes, the initiative
|
|
705
|
+
* formula and the status-effect array belong to the system. So there is no
|
|
706
|
+
* option here to replace them — `statusEffects` only appends, which is what
|
|
707
|
+
* a module is allowed to do.
|
|
708
|
+
*/
|
|
709
|
+
const registered$1 = /* @__PURE__ */ new Set();
|
|
710
|
+
/**
|
|
711
|
+
* The key Foundry files a module's document sub-type under.
|
|
712
|
+
*
|
|
713
|
+
* Use it wherever you name the type outside `registerModule` — registering the
|
|
714
|
+
* sheet, checking `actor.type`, writing `documentTypes` in the manifest. The
|
|
715
|
+
* prefix is easy to get wrong by hand and fails silently when you do.
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* ```ts
|
|
719
|
+
* moduleSubType('pdf-character-sheet', 'pdf'); // 'pdf-character-sheet.pdf'
|
|
720
|
+
* ```
|
|
721
|
+
*/
|
|
722
|
+
function moduleSubType(moduleId, type) {
|
|
723
|
+
return `${moduleId}.${type}`;
|
|
724
|
+
}
|
|
725
|
+
function vttfError$1(code, message) {
|
|
726
|
+
return new VttfError(code, message);
|
|
727
|
+
}
|
|
728
|
+
function readHooks$1() {
|
|
729
|
+
const hooks = globalThis.Hooks;
|
|
730
|
+
if (hooks === void 0 || typeof hooks.once !== "function") throw vttfError$1("VTTF-0002", "globalThis.Hooks is not available — call registerModule() inside a Foundry runtime or stub Hooks in tests");
|
|
731
|
+
return hooks;
|
|
732
|
+
}
|
|
733
|
+
function readConfig$1() {
|
|
734
|
+
const config = globalThis.CONFIG;
|
|
735
|
+
if (config === void 0) throw vttfError$1("VTTF-0002", "globalThis.CONFIG is not available — call registerModule() inside a Foundry runtime or stub CONFIG in tests");
|
|
736
|
+
return config;
|
|
737
|
+
}
|
|
738
|
+
function assignSubTypes(target, moduleId, models) {
|
|
739
|
+
for (const [type, model] of Object.entries(models)) target[moduleSubType(moduleId, type)] = model;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Register a Foundry module with VTTForge.
|
|
743
|
+
*
|
|
744
|
+
* Calling twice with the same `id` throws VTTF-0001 — almost always a
|
|
745
|
+
* hot-reload artefact or a duplicate import. The CONFIG mutations are deferred
|
|
746
|
+
* until Foundry's `init` hook fires.
|
|
747
|
+
*/
|
|
748
|
+
function registerModule(config) {
|
|
749
|
+
if (registered$1.has(config.id)) throw vttfError$1("VTTF-0001", `Module "${config.id}" was already registered`);
|
|
750
|
+
registered$1.add(config.id);
|
|
751
|
+
const hooks = readHooks$1();
|
|
752
|
+
hooks.once("init", () => {
|
|
753
|
+
applyInit$1(config);
|
|
754
|
+
});
|
|
755
|
+
if (config.onReady !== void 0) hooks.once("ready", () => {
|
|
756
|
+
config.onReady?.();
|
|
757
|
+
});
|
|
758
|
+
return config;
|
|
759
|
+
}
|
|
760
|
+
function applyInit$1(config) {
|
|
761
|
+
config.onBeforeInit?.();
|
|
762
|
+
const CONFIG = readConfig$1();
|
|
763
|
+
if (config.actorDataModels !== void 0) assignSubTypes(CONFIG.Actor.dataModels, config.id, config.actorDataModels);
|
|
764
|
+
if (config.itemDataModels !== void 0) assignSubTypes(CONFIG.Item.dataModels, config.id, config.itemDataModels);
|
|
765
|
+
if (config.statusEffects !== void 0 && config.statusEffects.length > 0) {
|
|
766
|
+
CONFIG.statusEffects ??= [];
|
|
767
|
+
CONFIG.statusEffects.push(...config.statusEffects);
|
|
768
|
+
}
|
|
769
|
+
config.onAfterInit?.();
|
|
770
|
+
}
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/register-system.ts
|
|
773
|
+
/**
|
|
774
|
+
* registerSystem — one call that replaces the boilerplate `Hooks.once("init", ...)`
|
|
775
|
+
* block in every Foundry system.
|
|
776
|
+
*
|
|
777
|
+
* Conforms to the canonical Foundry init lifecycle:
|
|
778
|
+
*
|
|
779
|
+
* init → CONFIG mutations (dataModels, documentClass, statusEffects)
|
|
780
|
+
* i18nInit → translate CONFIG labels
|
|
781
|
+
* setup → enrichers, packs
|
|
782
|
+
* ready → migrations (GM-only — consumer guards inside onReady)
|
|
783
|
+
*
|
|
784
|
+
* v0.1 scope: `init` + `ready`. `setup` / `i18nInit` callbacks remain v0.1.1.
|
|
785
|
+
*
|
|
786
|
+
* Per PRD §11 open question #1, we wrap the hook ourselves ("explicit hook for
|
|
787
|
+
* now"); callers don't need to write `Hooks.once("init", ...)` themselves.
|
|
788
|
+
*/
|
|
789
|
+
const registered = /* @__PURE__ */ new Set();
|
|
790
|
+
function readHooks() {
|
|
791
|
+
const hooks = globalThis.Hooks;
|
|
792
|
+
if (hooks === void 0 || typeof hooks.once !== "function") throw vttfError("VTTF-0002", "globalThis.Hooks is not available — call registerSystem() inside a Foundry runtime or stub Hooks in tests");
|
|
793
|
+
return hooks;
|
|
794
|
+
}
|
|
795
|
+
function readConfig() {
|
|
796
|
+
const config = globalThis.CONFIG;
|
|
797
|
+
if (config === void 0) throw vttfError("VTTF-0002", "globalThis.CONFIG is not available — call registerSystem() inside a Foundry runtime or stub CONFIG in tests");
|
|
798
|
+
return config;
|
|
799
|
+
}
|
|
800
|
+
function vttfError(code, message) {
|
|
801
|
+
return new VttfError(code, message);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Register a Foundry system with VTTForge. Idempotency: the same `id` calling
|
|
805
|
+
* twice throws VTTF-0001 — almost always a hot-reload or duplicate import bug.
|
|
806
|
+
*
|
|
807
|
+
* Returns the registration object so consumers can inspect what was applied
|
|
808
|
+
* (useful in tests). The actual CONFIG mutations are deferred until Foundry's
|
|
809
|
+
* `init` hook fires.
|
|
810
|
+
*/
|
|
811
|
+
function registerSystem(config) {
|
|
812
|
+
if (registered.has(config.id)) throw vttfError("VTTF-0001", `System "${config.id}" was already registered`);
|
|
813
|
+
registered.add(config.id);
|
|
814
|
+
const hooks = readHooks();
|
|
815
|
+
hooks.once("init", () => {
|
|
816
|
+
applyInit(config);
|
|
817
|
+
});
|
|
818
|
+
if (config.onReady !== void 0) hooks.once("ready", () => {
|
|
819
|
+
config.onReady?.();
|
|
820
|
+
});
|
|
821
|
+
return config;
|
|
822
|
+
}
|
|
823
|
+
function applyInit(config) {
|
|
824
|
+
config.onBeforeInit?.();
|
|
825
|
+
const CONFIG = readConfig();
|
|
826
|
+
if (config.actorDataModels !== void 0) Object.assign(CONFIG.Actor.dataModels, config.actorDataModels);
|
|
827
|
+
if (config.itemDataModels !== void 0) Object.assign(CONFIG.Item.dataModels, config.itemDataModels);
|
|
828
|
+
if (config.actorDocumentClass !== void 0) CONFIG.Actor.documentClass = config.actorDocumentClass;
|
|
829
|
+
if (config.itemDocumentClass !== void 0) CONFIG.Item.documentClass = config.itemDocumentClass;
|
|
830
|
+
if (config.combat?.initiative !== void 0) CONFIG.Combat.initiative = config.combat.initiative;
|
|
831
|
+
const legacyTransferral = config.activeEffect?.legacyTransferral ?? false;
|
|
832
|
+
CONFIG.ActiveEffect.legacyTransferral = legacyTransferral;
|
|
833
|
+
if (config.statusEffects !== void 0) CONFIG.statusEffects = [...config.statusEffects];
|
|
834
|
+
config.onAfterInit?.();
|
|
835
|
+
}
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region src/system-config.ts
|
|
838
|
+
/**
|
|
839
|
+
* SystemConfig — typed wrapper around `game.settings.register/get/set`.
|
|
840
|
+
*
|
|
841
|
+
* Eliminates the boilerplate of repeating the system id in every call:
|
|
842
|
+
*
|
|
843
|
+
* // before
|
|
844
|
+
* game.settings.register("ordemparanormal", "homebrewRules", { ... });
|
|
845
|
+
* game.settings.get("ordemparanormal", "homebrewRules");
|
|
846
|
+
*
|
|
847
|
+
* // after
|
|
848
|
+
* const cfg = new SystemConfig("ordemparanormal");
|
|
849
|
+
* cfg.register("homebrewRules", { ... });
|
|
850
|
+
* cfg.get<boolean>("homebrewRules");
|
|
851
|
+
*
|
|
852
|
+
* Also keeps a local manifest of registered keys so attempts to read an
|
|
853
|
+
* unregistered key fail with VTTF-0003 instead of returning undefined.
|
|
854
|
+
*
|
|
855
|
+
* Registration must happen during the `init` hook; reads can happen any
|
|
856
|
+
* time after.
|
|
857
|
+
*/
|
|
858
|
+
function readGame() {
|
|
859
|
+
const candidate = globalThis.game;
|
|
860
|
+
if (candidate === void 0 || candidate.settings === void 0) throw new VttfError("VTTF-0002", "game.settings is not available — call SystemConfig methods inside or after the Foundry \"init\" hook");
|
|
861
|
+
return candidate;
|
|
862
|
+
}
|
|
863
|
+
var SystemConfig = class {
|
|
864
|
+
systemId;
|
|
865
|
+
#registered = /* @__PURE__ */ new Set();
|
|
866
|
+
constructor(systemId) {
|
|
867
|
+
this.systemId = systemId;
|
|
868
|
+
}
|
|
869
|
+
register(key, config) {
|
|
870
|
+
readGame().settings.register(this.systemId, key, config);
|
|
871
|
+
this.#registered.add(key);
|
|
872
|
+
}
|
|
873
|
+
get(key) {
|
|
874
|
+
if (!this.#registered.has(key)) throw new VttfError("VTTF-0003", `SystemConfig.get("${key}") was called before register("${key}")`);
|
|
875
|
+
return readGame().settings.get(this.systemId, key);
|
|
876
|
+
}
|
|
877
|
+
async set(key, value) {
|
|
878
|
+
if (!this.#registered.has(key)) throw new VttfError("VTTF-0003", `SystemConfig.set("${key}") was called before register("${key}")`);
|
|
879
|
+
return readGame().settings.set(this.systemId, key, value);
|
|
880
|
+
}
|
|
881
|
+
isRegistered(key) {
|
|
882
|
+
return this.#registered.has(key);
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
//#endregion
|
|
886
|
+
//#region src/index.ts
|
|
887
|
+
/**
|
|
888
|
+
* @vttforge/core — runtime utilities for FoundryVTT v13+ systems and modules.
|
|
889
|
+
*
|
|
890
|
+
* v0.1 surface:
|
|
891
|
+
*
|
|
892
|
+
* - registerSystem() — one-call init, replaces Hooks.once("init")
|
|
893
|
+
* - registerModule() — the same for modules, with namespaced sub-types
|
|
894
|
+
* - SystemConfig — typed wrapper around game.settings
|
|
895
|
+
* - BaseTypeDataModel() — TypeDataModel with safe migrateData default
|
|
896
|
+
* - BaseActorSheet() — ActorSheetV2 + HandlebarsApplicationMixin
|
|
897
|
+
* - BaseItemSheet() — ItemSheetV2 + HandlebarsApplicationMixin
|
|
898
|
+
* - fields() — typed bag of foundry.data.fields constructors
|
|
899
|
+
* - InferSchema<T> — derive `system` shape from defineSchema()
|
|
900
|
+
* - createMigrationRunner() — declarative schema migrations (register + run)
|
|
901
|
+
* - VttfError + error registry — VTTF-NNNN codes with docs URLs
|
|
902
|
+
*
|
|
903
|
+
* Foundry classes are resolved from `globalThis.foundry` lazily so the package
|
|
904
|
+
* imports cleanly in Node/tests; concrete Foundry typing arrives with
|
|
905
|
+
* `@vttforge/types` in v1.0.
|
|
906
|
+
*/
|
|
907
|
+
const VTTFORGE_CORE_VERSION = "0.2.0";
|
|
908
|
+
//#endregion
|
|
909
|
+
export { BaseActorSheet, BaseItemSheet, BaseTypeDataModel, ERROR_MANIFEST_VERSION, SystemConfig, VTTFORGE_CORE_VERSION, VTTFORGE_SHEET_CLASS, VttfError, createMigrationRunner, docsUrlFor, fields, getErrorEntry, getErrorManifest, listErrorEntries, moduleSubType, registerModule, registerSystem };
|
|
910
|
+
|
|
911
|
+
//# sourceMappingURL=index.mjs.map
|