@mapletip/data 0.3.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.
Files changed (43) hide show
  1. package/README.md +216 -2
  2. package/client/contract-compatibility.mjs +10 -0
  3. package/client/items.d.ts +28 -5
  4. package/client/items.js +94 -5
  5. package/client/map-geography.d.ts +4 -0
  6. package/client/map-geography.js +2 -0
  7. package/client/maps.d.ts +22 -0
  8. package/client/maps.js +25 -0
  9. package/client/monster-client.d.ts +22 -3
  10. package/client/monster-client.js +115 -5
  11. package/client/monsters.d.ts +33 -7
  12. package/client/monsters.js +36 -4
  13. package/client/static-client.d.ts +7 -5
  14. package/client/static-client.js +3 -2
  15. package/client/static-domains.d.ts +8 -1
  16. package/client/static-domains.js +27 -3
  17. package/generated/item-browse-pack-2.d.ts +83 -1
  18. package/generated/item-browse-pack-2.validator.js +2 -2
  19. package/generated/item-related-entity-browse-1.d.ts +48 -0
  20. package/generated/item-related-entity-browse-1.validator.js +1 -0
  21. package/generated/map-catalog-2-1.d.ts +103 -0
  22. package/generated/map-catalog-2-1.validator.d.ts +18 -0
  23. package/generated/map-catalog-2-1.validator.js +1 -0
  24. package/generated/map-geography.d.ts +79 -0
  25. package/generated/map-geography.validator.d.ts +18 -0
  26. package/generated/map-geography.validator.js +1 -0
  27. package/generated/monster-catalog-2-2.d.ts +332 -0
  28. package/generated/monster-catalog-2-2.validator.js +17 -0
  29. package/generated/monster-catalog-2-3.d.ts +339 -0
  30. package/generated/monster-catalog-2-3.validator.js +17 -0
  31. package/generated/monster-catalog-2-4.d.ts +378 -0
  32. package/generated/monster-catalog-2-4.validator.js +17 -0
  33. package/generated/monster-drop-browse-1-1.d.ts +150 -0
  34. package/generated/monster-drop-browse-1-1.validator.js +17 -0
  35. package/generated/monster-drop-browse.d.ts +123 -0
  36. package/generated/monster-drop-browse.validator.js +17 -0
  37. package/generated/monster-location-browse-1-1.d.ts +79 -0
  38. package/generated/monster-location-browse-1-1.validator.js +17 -0
  39. package/generated/monster-location-browse.d.ts +69 -0
  40. package/generated/monster-location-browse.validator.js +17 -0
  41. package/generated/product-manifest.d.ts +23 -0
  42. package/generated/product-manifest.validator.js +1 -1
  43. package/package.json +40 -2
package/README.md CHANGED
@@ -82,11 +82,141 @@ digest. `getMonsterBrowse` still requests only the release manifest named by the
82
82
  build index, the product manifest and the Monster catalog.
83
83
 
84
84
  Releases built before revision `2.1` carry contract-`2.0` catalogs without `sprite`; this
85
- SDK reads both, and `hasMonsterSprites` narrows a catalog to `2.1`. `@mapletip/data`
85
+ SDK reads both, and `hasMonsterSprites` narrows a catalog to revisions `2.1` through `2.4`. `@mapletip/data`
86
86
  `0.2.0` and older reject a `2.1` catalog with a typed `SCHEMA_VALIDATION_ERROR` instead of
87
87
  returning rows without sprites, so upgrade to `0.3.0` before adopting a pin that carries
88
88
  it.
89
89
 
90
+ ### Monster HP and MP recovery
91
+
92
+ Catalog revision `2.4` adds independent `hpRecovery` and `mpRecovery` facts to each row.
93
+ `known` preserves the source integer, including zero; `unknown`, `absent` with its
94
+ coverage key, and `unsupported` remain distinct. One field's value or state says nothing
95
+ about the other. No recovery interval or per-second unit is established, so describe
96
+ these as HP and MP recovery values rather than rates.
97
+
98
+ ```ts
99
+ import { getMonsterBrowse } from '@mapletip/data/monsters';
100
+
101
+ const catalog = await getMonsterBrowse({ baseUrl: releaseRootUrl, pin: releasePin, buildId: 'gms-v83' });
102
+ if (catalog.format === 'mapletip.monster-catalog-2' && catalog.contractVersion === '2.4') {
103
+ const recoveries = catalog.records.map((row) => ({
104
+ hpRecovery: row.hpRecovery.state === 'known' ? row.hpRecovery.value : row.hpRecovery.state,
105
+ mpRecovery: row.mpRecovery.state === 'known' ? row.mpRecovery.value : row.mpRecovery.state,
106
+ }));
107
+ }
108
+ ```
109
+
110
+ ### Monster drops and locations
111
+
112
+ Monster catalog `2.2` keeps these summaries on every Monster row. The catalog-level
113
+ `browseCoverage` carries the shared witnesses and conventions: `nativeDropsAbsentCoverageKey`
114
+ is present exactly when at least one native drop row is `absent`; `historicalDrops` says whether
115
+ the build has a historical overlay; `locationsAbsentCoverageKey` is present exactly when at
116
+ least one location row is `absent`; and `locationScope` is `client-placements`. An absent row
117
+ does not repeat the catalog coverage key.
118
+
119
+ `drops.native` describes evidence from the selected build. Its `lowerBound: true` marker
120
+ says a `known` count is a lower bound while native source coverage is partial.
121
+ `unknown` means the count could not be established; it does not mean zero.
122
+ `drops.historical` is present only when the build
123
+ advertises historical overlay support. CW Launch reports native data separately from
124
+ its historical entries, so a Monster can have no native data and still have historical
125
+ drop assertions. GMS v83 reports historical drops as unsupported.
126
+
127
+ The drop browse artifact stores each distinct item once in its sorted `items[]` table.
128
+ Each record entry's `item` is an index into that table. Native entries describe this
129
+ build; historical entries also carry `sourceBuild` and `confidence`. A resolved
130
+ Equipment category is `{ kind: "equip-slot", code }`; display labels for that code come
131
+ from the same build's Equipment product slot labels. Other resolved item categories use
132
+ `{ kind: "item-class", value }`, where the value is `ItemBrowseClass`. Unresolved item
133
+ references use `{ kind: "unknown" }`.
134
+
135
+ `detail.dropEvidence.state` keeps its existing native-only meaning. It describes the
136
+ native evidence collection for the Monster and is not changed by historical overlay
137
+ assertions. Read the compact historical assertions from the drop browse artifact:
138
+
139
+ ```ts
140
+ import { getMonsterDropBrowse } from '@mapletip/data/monsters';
141
+
142
+ const dropBrowse = await getMonsterDropBrowse({
143
+ baseUrl: releaseRootUrl,
144
+ pin: releasePin,
145
+ buildId: 'cw-launch',
146
+ });
147
+ const itemKey = 'item-0123456789abcdef';
148
+ const monstersWithItem = Object.entries(dropBrowse.records)
149
+ .filter(([, record]) => record?.entries.some((entry) => {
150
+ const item = dropBrowse.items[entry.item];
151
+ return item?.itemRef.state === 'resolved' && item.itemRef.key === itemKey;
152
+ }))
153
+ .map(([monsterKey]) => monsterKey);
154
+ ```
155
+
156
+ Drop browse revision `1.1` adds each resolved distinct item's same-build Media icon fact to
157
+ the `items[]` table. Use `itemIconUrl` only for a `known` icon; for other states, keep the
158
+ item name visible without substituting another image. The icon is already in the drop
159
+ browse artifact, so this read does not need an Item, Equipment or Media request:
160
+
161
+ ```ts
162
+ import { getMonsterDropBrowse } from '@mapletip/data/monsters';
163
+ import { itemIconUrl } from '@mapletip/data/items';
164
+
165
+ const dropBrowse = await getMonsterDropBrowse({
166
+ baseUrl: releaseRootUrl,
167
+ pin: releasePin,
168
+ buildId: 'cw-launch',
169
+ });
170
+ if (dropBrowse.contractVersion === '1.1') {
171
+ const items = dropBrowse.items.map((item) => ({
172
+ name: item.itemName.state === 'known' ? item.itemName.value : 'Unknown item',
173
+ iconUrl: item.icon.state === 'known'
174
+ ? itemIconUrl(releaseRootUrl, item.icon.value)
175
+ : undefined,
176
+ }));
177
+ }
178
+ ```
179
+
180
+ Location summaries count client map placements, not live spawn rates. `known` carries
181
+ distinct map and placement counts; `absent` carries neither count and uses the
182
+ catalog-level coverage key; `unresolved` carries counts for the resolved portion when
183
+ some placements could not be attributed. Location browse entries carry the public
184
+ `mapKey`, map name and placement count, with no coordinates or media references.
185
+ `regionCode` is the raw source classification code, not a geographic region.
186
+
187
+ Filter by map key, then resolve that map's minimap through the same build's Media product.
188
+ The Media record key is the map key; its `map.minimap` descriptor resolves to a
189
+ content-addressed image URL:
190
+
191
+ ```ts
192
+ import { getMonsterLocationBrowse } from '@mapletip/data/monsters';
193
+ import { openRelease } from '@mapletip/data/release';
194
+ import { resolveMediaUrl } from '@mapletip/data/media';
195
+
196
+ const buildId = 'gms-v83';
197
+ const locationBrowse = await getMonsterLocationBrowse({
198
+ baseUrl: releaseRootUrl,
199
+ pin: releasePin,
200
+ buildId,
201
+ });
202
+ const mapKey = 'map-0123456789abcdef';
203
+ const monstersOnMap = Object.entries(locationBrowse.records)
204
+ .filter(([, record]) => record?.entries.some((entry) => entry.mapKey === mapKey))
205
+ .map(([monsterKey]) => monsterKey);
206
+
207
+ const release = await openRelease({ baseUrl: releaseRootUrl, pin: releasePin });
208
+ const media = await release.openMediaBuild(buildId);
209
+ const mapMedia = (await media.loadCatalog()).records.find((record) => record.key === mapKey);
210
+ const minimap = mapMedia?.media['map.minimap'];
211
+ const minimapUrl = minimap?.state === 'known'
212
+ ? resolveMediaUrl(releaseRootUrl, minimap.value)
213
+ : undefined;
214
+ ```
215
+
216
+ `0.3.0` reads catalog revisions `2.0` and `2.1` but rejects `2.2` with a typed
217
+ `SCHEMA_VALIDATION_ERROR`. Use `0.4.0` before adopting a release with drop and location
218
+ browse summaries.
219
+
90
220
  `pin` is the parsed `pin.json` published next to the release root by whoever assembled
91
221
  the release: it names the manifest path, SHA-256 and byte length and is the caller's
92
222
  trust anchor. The SDK never resolves a "current" release on its own and never fetches a
@@ -145,6 +275,66 @@ It rejects any content ID that is not a 64-character lowercase SHA-256 digest wi
145
275
  `SCHEMA_VALIDATION_ERROR`, so a row's `icon.value` can be used without building a
146
276
  make-believe descriptor.
147
277
 
278
+ ### Player facts and related identities
279
+
280
+ Item Browse 2.1 adds sparse typed player facts. An omitted sparse fact means its value is
281
+ unknown, unless that field's per-build coverage says `unsupported`; it does not mean
282
+ zero. A recorded `sellPrice: 0` is a real zero. Use
283
+ `hasItemPlayerFacts` to narrow a browse result before relying on the 2.1 coverage block;
284
+ older pinned 2.0 products remain readable. `coverage.playerFacts.requirementJobs` is
285
+ `unsupported` in both current builds because neither has a normalized non-equipment job
286
+ requirement source. NPC shop purchase prices are deferred: sell price is not a shop price.
287
+
288
+ `requirement.level` is the item's direct use-level requirement. The separate
289
+ `levelRange` fact carries GMS Monster Crystal `monsterLevelMin`/`monsterLevelMax` and
290
+ Secret Crystal `requirementLevelDelta`; these values are never folded into
291
+ `requirement.level`. Recovery keeps flat `recovery.hp`/`mp` distinct from percentage
292
+ `recovery.hpPercent`/`mpPercent`. Buff effects use the closed `ItemBrowseBuffStat`
293
+ vocabulary, and unknown source `spec` keys fail the publication gate unless they have
294
+ an explicit exclusion.
295
+
296
+ Related identities are stored in one lazy roster artifact per relationship kind. The
297
+ Items manifest pins each artifact by digest and byte length. In a roster,
298
+ `targets` is the target index table and `records[itemKey]` contains indices into that
299
+ table. Load only the kind needed for the current view. For example, this resolves the
300
+ `Dropped by` names for one item without loading all seven rosters:
301
+
302
+ ```ts
303
+ import {
304
+ getItemBrowse,
305
+ getItemRelatedEntities,
306
+ hasItemPlayerFacts,
307
+ } from '@mapletip/data/items';
308
+ import type { GetItemBrowseOptions } from '@mapletip/data/items';
309
+
310
+ declare const releaseRootUrl: GetItemBrowseOptions['baseUrl'];
311
+ declare const releasePin: GetItemBrowseOptions['pin'];
312
+
313
+ const browse = await getItemBrowse({
314
+ baseUrl: releaseRootUrl,
315
+ pin: releasePin,
316
+ buildId: 'gms-v83',
317
+ });
318
+ if (!hasItemPlayerFacts(browse)) throw new Error('this pin predates Item player facts');
319
+
320
+ const item = browse.records.find((row) => row.sourceId === '02022001');
321
+ if (!item) throw new Error('item is not present in this build');
322
+ const roster = await getItemRelatedEntities({
323
+ baseUrl: releaseRootUrl,
324
+ pin: releasePin,
325
+ buildId: 'gms-v83',
326
+ kind: 'monster-drop',
327
+ });
328
+ const droppedBy = (roster.records[item.key] ?? []).flatMap((targetIndex) => {
329
+ const target = roster.targets[targetIndex];
330
+ return target ? [{ key: target.key, name: target.displayName ?? target.key }] : [];
331
+ });
332
+ ```
333
+
334
+ The `droppedBy` result can be rendered as the row's `Dropped by` names. Target names may
335
+ be unknown, in which case the stable target key remains available. The roster read is
336
+ digest-verified, build-scoped, uncached and has no fallback to another build.
337
+
148
338
  ## What each entry loads
149
339
 
150
340
  Monster catalog validation and the release/manifest/index validators are imported
@@ -154,7 +344,7 @@ operation that needs it, as a separate on-demand chunk:
154
344
  | Entry | Eager generated validators | Loaded on demand |
155
345
  | --- | --- | --- |
156
346
  | `/monsters` | release pin, release manifest, build index, product manifest, Monster catalog 1.x, 2.0 and 2.1 | Monster detail, location/relationship, core-relationship and evidence packs |
157
- | `/items` | product manifest only (every Item validator is on demand) | browse, catalog, detail, relationship and evidence packs, plus the release client behind `getItemBrowse` |
347
+ | `/items` | product manifest only (every Item validator is on demand) | browse, per-kind related-entity roster, catalog, detail, relationship and evidence packs, plus the release client behind `getItemBrowse` and `getItemRelatedEntities` |
158
348
  | `/equipment` | that domain's catalog, detail, relationship and evidence packs | — |
159
349
  | `/quests` | none (Quest validators are always on demand) | catalog, browse, detail, relationship, auxiliary and evidence packs |
160
350
  | `/media` | that domain's catalog, detail packs | — |
@@ -288,3 +478,27 @@ const evPage = await client.getEvidencePage('quest-ev-1000');
288
478
 
289
479
  The Quest client supports lazy per-operation validator loading via dynamic `import()` to ensure that only the validator required for a specific operation is fetched. Browse records and detail records are lazily addressed through bounded key-range descriptors without per-record eager loads or mandatory catalog fetching for direct detail lookups.
290
480
 
481
+ ## Map geography
482
+
483
+ Map geography uses a build-scoped, digest-verified read model. `getMapGeography` reads that model from the `maps` product; `openMapProduct` returns a product with `loadGeography()`, and `release.openDomainBuild(buildId, 'maps')` provides the same operation when an opened release is reused. Map catalog rows carry compact geography keys and provenance, while the geography model stores the labels once.
484
+
485
+ The derivation layers run in order: reviewed placements, the build-family region allowlist, area names from Map marks (Hidden Street marks are excluded), then collection classification. Maps with no applicable layer have provenance `none`. Provenance values are `reviewed-overlay`, `source-region-allowlist`, `source-map-mark`, `source-classification`, and `none`.
486
+
487
+ Reviewed placement status `supported` means local witnesses support the entrance and placement. Citation register entries that could not be fetched remain inactive; they are not treated as verified external evidence. The carried-over placements in these builds use `supported` status.
488
+
489
+ Geography keys are stable filters: `region:<slug>`, `area:<region-slug>/<area-slug>`, and `collection:<slug>`. Monster catalog 2.3 rows contain the union of their location entries' region and location keys. A Region filter can use those keys directly:
490
+
491
+ ```ts
492
+ import { getMonsterBrowse, hasMonsterGeography } from '@mapletip/data/monsters';
493
+
494
+ const catalog = await getMonsterBrowse({
495
+ baseUrl: releaseRootUrl,
496
+ pin: releasePin,
497
+ buildId: 'gms-v83',
498
+ });
499
+ if (!hasMonsterGeography(catalog)) throw new Error('This release predates Monster geography');
500
+
501
+ const regionKey = 'region:henesys';
502
+ const monstersInRegion = catalog.records.filter((row) => row.geography.regionKeys.includes(regionKey));
503
+ ```
504
+
@@ -8,11 +8,16 @@ const MINIMUM_VERSION_BY_CAPABILITY = new Map([
8
8
  ['product-input-identity-v1', { major: 1, minor: 2 }],
9
9
  ['release-manifest-v1', { major: 1, minor: 2 }],
10
10
  ['monster-core-v1', { major: 2, minor: 0 }],
11
+ ['monster-drop-browse-v1', { major: 2, minor: 0 }],
12
+ ['monster-location-browse-v1', { major: 2, minor: 0 }],
13
+ ['item-player-facts-v1', { major: 2, minor: 0 }],
14
+ ['item-related-entities-v1', { major: 2, minor: 0 }],
11
15
  ['quests-v1', { major: 2, minor: 0 }],
12
16
  ['items-v1', { major: 2, minor: 0 }],
13
17
  ['equipment-v1', { major: 2, minor: 0 }],
14
18
  ['media-v1', { major: 2, minor: 0 }],
15
19
  ['maps-v1', { major: 2, minor: 0 }],
20
+ ['map-geography-v1', { major: 2, minor: 0 }],
16
21
  ['npcs-v1', { major: 2, minor: 0 }],
17
22
  ['reactors-v1', { major: 2, minor: 0 }],
18
23
  ['skills-v1', { major: 2, minor: 0 }],
@@ -33,11 +38,16 @@ const supportedClientCapabilities = new Set([
33
38
  'product-input-identity-v1',
34
39
  'release-manifest-v1',
35
40
  'monster-core-v1',
41
+ 'monster-drop-browse-v1',
42
+ 'monster-location-browse-v1',
43
+ 'item-player-facts-v1',
44
+ 'item-related-entities-v1',
36
45
  'quests-v1',
37
46
  'items-v1',
38
47
  'equipment-v1',
39
48
  'media-v1',
40
49
  'maps-v1',
50
+ 'map-geography-v1',
41
51
  'npcs-v1',
42
52
  'reactors-v1',
43
53
  'skills-v1',
package/client/items.d.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * client is imported on demand, so a browse-only consumer never downloads catalog,
6
6
  * detail, relationship, evidence validators or the Monster client.
7
7
  */
8
- import type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowseRecord } from '../generated/item-browse-pack-2.d.ts';
8
+ import type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowsePlayerFactCoverage, ItemBrowseBuff, ItemBrowseBuffEffect, ItemBrowseBuffStat, ItemBrowseBuffUnit, ItemBrowseCoverageState, ItemBrowseJobFamily, ItemBrowseLevelRange, ItemBrowseRecovery, ItemBrowseRequirement, ItemBrowseRecord } from '../generated/item-browse-pack-2.d.ts';
9
+ import type { ItemBrowseRelationshipKind, ItemRelatedEntityBrowsePack, ItemRelatedEntityTarget } from '../generated/item-related-entity-browse-1.d.ts';
9
10
  import type { ItemCatalog } from '../generated/item-catalog-2.d.ts';
10
11
  import type { ItemDetailRecord } from '../generated/item-detail-2.d.ts';
11
12
  import type { ItemRelationshipPage } from '../generated/item-relationship-pack-2.d.ts';
@@ -15,22 +16,37 @@ import { type GetRelationshipOptions, type OpenProductOptions, type OperationOpt
15
16
  export type StaticItemCatalog = ItemCatalog;
16
17
  export type StaticItemDetail = ItemDetailRecord;
17
18
  export type { GetRelationshipOptions };
18
- export type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowseRecord };
19
+ export type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowsePlayerFactCoverage, ItemBrowseBuff, ItemBrowseBuffEffect, ItemBrowseBuffStat, ItemBrowseBuffUnit, ItemBrowseCoverageState, ItemBrowseJobFamily, ItemBrowseLevelRange, ItemBrowseRecovery, ItemBrowseRequirement, ItemBrowseRecord, ItemBrowseRelationshipKind, ItemRelatedEntityBrowsePack, ItemRelatedEntityTarget, };
19
20
  export type { OpenReleaseOptions };
20
21
  /** One browse row: the compact record plus its stable public key. */
21
22
  export type ItemBrowseRow = ItemBrowseRecord & {
22
23
  readonly key: string;
23
24
  };
24
- /** The whole verified browse roster of one Item build. */
25
- export interface StaticItemBrowse {
25
+ /** Verified Item Browse 2.0, retained for older pinned products. */
26
+ export interface StaticItemBrowseV2_0 {
27
+ readonly contractVersion: '2.0';
28
+ readonly scope: ProductScope;
29
+ readonly coverage: Omit<ItemBrowseCoverage, 'playerFacts'>;
30
+ readonly records: readonly ItemBrowseRow[];
31
+ }
32
+ /** Verified Item Browse 2.1 with its per-build player-fact coverage. */
33
+ export interface StaticItemBrowseV2_1 {
34
+ readonly contractVersion: '2.1';
26
35
  readonly scope: ProductScope;
27
- readonly coverage: ItemBrowseCoverage;
36
+ readonly coverage: ItemBrowseCoverage & {
37
+ readonly playerFacts: ItemBrowsePlayerFactCoverage;
38
+ };
28
39
  readonly records: readonly ItemBrowseRow[];
29
40
  }
41
+ /** The whole verified browse roster of one Item build. */
42
+ export type StaticItemBrowse = StaticItemBrowseV2_0 | StaticItemBrowseV2_1;
43
+ /** Narrow an Item browse result to contract 2.1, which carries player-fact coverage. */
44
+ export declare function hasItemPlayerFacts(browse: StaticItemBrowse): browse is StaticItemBrowseV2_1;
30
45
  export interface StaticItemProductClient {
31
46
  readonly manifest: StaticProductManifest;
32
47
  loadCatalog(options?: OperationOptions): Promise<StaticItemCatalog>;
33
48
  loadBrowse(options?: OperationOptions): Promise<StaticItemBrowse>;
49
+ loadRelatedEntities(kind: ItemBrowseRelationshipKind, options?: OperationOptions): Promise<ItemRelatedEntityBrowsePack>;
34
50
  getDetail(key: string, options?: OperationOptions): Promise<StaticItemDetail>;
35
51
  getRelationshipPage(pageKey: string, options?: GetRelationshipOptions): Promise<ItemRelationshipPage>;
36
52
  getEvidencePage(pageKey: string, options?: OperationOptions): Promise<ItemEvidencePage>;
@@ -54,5 +70,12 @@ export type GetItemBrowseOptions = OpenReleaseOptions & {
54
70
  * client is imported on demand so the Item browse entry stays narrow.
55
71
  */
56
72
  export declare function getItemBrowse(options: GetItemBrowseOptions): Promise<StaticItemBrowse>;
73
+ /** Options for one verified, one-shot related-entity roster read. */
74
+ export type GetItemRelatedEntitiesOptions = OpenReleaseOptions & {
75
+ readonly buildId: string;
76
+ readonly kind: ItemBrowseRelationshipKind;
77
+ };
78
+ /** Read one build's manifest-pinned roster for exactly one Item relationship kind. */
79
+ export declare function getItemRelatedEntities(options: GetItemRelatedEntitiesOptions): Promise<ItemRelatedEntityBrowsePack>;
57
80
  /** Resolve one Item icon blob URL from its content-addressed media digest. */
58
81
  export declare function itemIconUrl(baseUrl: URL | string, contentId: string): string;
package/client/items.js CHANGED
@@ -1,5 +1,10 @@
1
- import { MissingPageError, MissingRecordError, SchemaValidationError, ScopeError, UnsupportedCapabilityError, UnsupportedProductError, lazyValidator, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, resolveMediaContentUrl, } from './core.js';
1
+ import { MissingPageError, MissingRecordError, SchemaValidationError, ScopeError, UnsupportedCapabilityError, UnsupportedProductError, assertSameScope, lazyValidator, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, resolveMediaContentUrl, } from './core.js';
2
+ /** Narrow an Item browse result to contract 2.1, which carries player-fact coverage. */
3
+ export function hasItemPlayerFacts(browse) {
4
+ return browse.contractVersion === '2.1' && browse.coverage.playerFacts !== undefined;
5
+ }
2
6
  const loadBrowsePackValidator = lazyValidator(() => import('../generated/item-browse-pack-2.validator.js'));
7
+ const loadRelatedEntityBrowseValidator = lazyValidator(() => import('../generated/item-related-entity-browse-1.validator.js'));
3
8
  const loadCatalogValidator = lazyValidator(() => import('../generated/item-catalog-2.validator.js'));
4
9
  const loadDetailValidator = lazyValidator(() => import('../generated/item-detail-2.validator.js'));
5
10
  const loadDetailPackValidator = lazyValidator(() => import('../generated/item-pack-2.validator.js'));
@@ -7,7 +12,7 @@ const loadRelationshipPackValidator = lazyValidator(() => import('../generated/i
7
12
  const loadEvidencePackValidator = lazyValidator(() => import('../generated/item-evidence-pack-2.validator.js'));
8
13
  /** Build an Item client over an already verified product manifest. */
9
14
  export function itemProductClient(context) {
10
- const { manifest, fetchEnvelope } = context;
15
+ const { manifest, fetchJson, fetchEnvelope } = context;
11
16
  if (manifest.productId !== 'items')
12
17
  throw new UnsupportedProductError(manifest.productId);
13
18
  let catalogPromise;
@@ -37,9 +42,13 @@ export function itemProductClient(context) {
37
42
  const rows = [];
38
43
  let previousKey;
39
44
  const coverage = packs[0].coverage;
45
+ const contractVersion = packs[0].contractVersion;
40
46
  const coverageJson = JSON.stringify(coverage);
41
47
  for (const [index, pack] of packs.entries()) {
42
48
  const descriptor = descriptors[index];
49
+ if (pack.contractVersion !== contractVersion) {
50
+ throw new SchemaValidationError(descriptor.path, 'browse pack contract version does not match the first browse pack');
51
+ }
43
52
  const keys = Object.keys(pack.records);
44
53
  if (keys.length !== descriptor.count) {
45
54
  throw new SchemaValidationError(descriptor.path, `browse pack has ${keys.length} records but its descriptor declares ${descriptor.count}`);
@@ -68,12 +77,51 @@ export function itemProductClient(context) {
68
77
  if (rows.length !== manifest.totalEntityCount) {
69
78
  throw new SchemaValidationError(manifest.catalog.path, `browse roster has ${rows.length} rows but the manifest declares ${manifest.totalEntityCount}`);
70
79
  }
71
- return { scope: manifest.scope, coverage, records: rows };
80
+ if (contractVersion === '2.1') {
81
+ const playerFacts = coverage.playerFacts;
82
+ if (!playerFacts) {
83
+ throw new SchemaValidationError(descriptors[0].path, 'Item Browse 2.1 is missing player-fact coverage');
84
+ }
85
+ return {
86
+ contractVersion,
87
+ scope: manifest.scope,
88
+ coverage: { ...coverage, playerFacts },
89
+ records: rows,
90
+ };
91
+ }
92
+ if (contractVersion === '2.0') {
93
+ if ((manifest.requiredClientCapabilities ?? []).includes('item-player-facts-v1')) {
94
+ throw new SchemaValidationError(descriptors[0].path, 'Item product requires player facts but the browse pack is contract 2.0');
95
+ }
96
+ const { playerFacts: _playerFacts, ...legacyCoverage } = coverage;
97
+ return { contractVersion, scope: manifest.scope, coverage: legacyCoverage, records: rows };
98
+ }
99
+ throw new SchemaValidationError(descriptors[0].path, `unsupported Item Browse contract ${contractVersion}`);
72
100
  })();
73
101
  void browsePromise.catch(() => { browsePromise = undefined; });
74
102
  }
75
103
  return browsePromise;
76
104
  }
105
+ async function loadRelatedEntities(kind, operation) {
106
+ if (!(manifest.requiredClientCapabilities ?? []).includes('item-related-entities-v1')) {
107
+ throw new UnsupportedCapabilityError('Item product manifest does not require item-related-entities-v1');
108
+ }
109
+ const descriptor = manifest.relatedEntityBrowsePacks?.[kind];
110
+ if (!descriptor) {
111
+ throw new UnsupportedCapabilityError(`Item product manifest does not provide ${kind} related entities`);
112
+ }
113
+ const validator = await loadRelatedEntityBrowseValidator();
114
+ const raw = await fetchJson(descriptor, 'browse', operation);
115
+ if (!validator(raw)) {
116
+ throw new SchemaValidationError(descriptor.path, JSON.stringify(validator.errors ?? []));
117
+ }
118
+ const pack = raw;
119
+ assertSameScope(pack.scope, manifest.scope, descriptor.path);
120
+ if (pack.relationshipKind !== kind) {
121
+ throw new SchemaValidationError(descriptor.path, `related-entity roster kind ${pack.relationshipKind} does not match requested ${kind}`);
122
+ }
123
+ return pack;
124
+ }
77
125
  async function getDetail(key, operation) {
78
126
  const descriptor = recordRangeDescriptor(manifest.detailPacks, key, 'detail', 'Item');
79
127
  const packValidator = await loadDetailPackValidator();
@@ -110,7 +158,7 @@ export function itemProductClient(context) {
110
158
  throw new MissingPageError(pageKey, 'evidence');
111
159
  return page;
112
160
  }
113
- return { manifest, loadCatalog, loadBrowse, getDetail, getRelationshipPage, getEvidencePage };
161
+ return { manifest, loadCatalog, loadBrowse, loadRelatedEntities, getDetail, getRelationshipPage, getEvidencePage };
114
162
  }
115
163
  /** Open one pinned Item product. */
116
164
  export async function openItemProduct(options) {
@@ -132,7 +180,48 @@ export async function getItemBrowse(options) {
132
180
  const items = await release.openItemBuild(options.buildId);
133
181
  return items.loadBrowse({ signal: options.signal });
134
182
  }
183
+ /** Read one build's manifest-pinned roster for exactly one Item relationship kind. */
184
+ export async function getItemRelatedEntities(options) {
185
+ const { openRelease } = await import('./release.js');
186
+ const release = await openRelease(options);
187
+ const items = await release.openItemBuild(options.buildId);
188
+ return items.loadRelatedEntities(options.kind, { signal: options.signal });
189
+ }
135
190
  /** Resolve one Item icon blob URL from its content-addressed media digest. */
136
191
  export function itemIconUrl(baseUrl, contentId) {
137
- return resolveMediaContentUrl(baseUrl, contentId, 'Item icon');
192
+ return resolveMediaContentUrl(baseUrl, compactSha256ToHex(contentId) ?? contentId, 'Item icon');
193
+ }
194
+ /** Expand the drop-browse base64url digest while keeping the resolver's hex contract. */
195
+ function compactSha256ToHex(value) {
196
+ if (value.length !== 43)
197
+ return undefined;
198
+ const bytes = [];
199
+ let accumulator = 0;
200
+ let bitCount = 0;
201
+ for (let index = 0; index < value.length; index += 1) {
202
+ const code = value.charCodeAt(index);
203
+ const sextet = code >= 65 && code <= 90
204
+ ? code - 65
205
+ : code >= 97 && code <= 122
206
+ ? code - 97 + 26
207
+ : code >= 48 && code <= 57
208
+ ? code - 48 + 52
209
+ : code === 45
210
+ ? 62
211
+ : code === 95
212
+ ? 63
213
+ : -1;
214
+ if (sextet < 0)
215
+ return undefined;
216
+ accumulator = (accumulator << 6) | sextet;
217
+ bitCount += 6;
218
+ if (bitCount >= 8) {
219
+ bitCount -= 8;
220
+ bytes.push((accumulator >> bitCount) & 0xff);
221
+ accumulator &= (1 << bitCount) - 1;
222
+ }
223
+ }
224
+ if (bytes.length !== 32 || bitCount !== 2 || accumulator !== 0)
225
+ return undefined;
226
+ return bytes.map((byte) => byte.toString(16).padStart(2, '0')).join('');
138
227
  }
@@ -0,0 +1,4 @@
1
+ export type { MapGeographyFact, MapGeographyProvenance, MapGeographyReviewStatus, } from '../generated/map-catalog-2-1.d.ts';
2
+ export type { MapGeographyArea, MapGeographyCitation, MapGeographyCollection, MapGeographyEntryPoint, MapGeographyModel, MapGeographyRegion, MapGeographyReviewedPlacement, MapGeographyWitnessCount, } from '../generated/map-geography.d.ts';
3
+ export { default as validateMapCatalogV2_1 } from '../generated/map-catalog-2-1.validator.js';
4
+ export { default as validateMapGeography } from '../generated/map-geography.validator.js';
@@ -0,0 +1,2 @@
1
+ export { default as validateMapCatalogV2_1 } from '../generated/map-catalog-2-1.validator.js';
2
+ export { default as validateMapGeography } from '../generated/map-geography.validator.js';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `@mapletip/data/maps` — verified Map geography and Map catalog reads.
3
+ */
4
+ import { isMapGeographyModel, type MapGeographyModel, type StaticDomainProductClient } from './static-domains.js';
5
+ import { type OpenProductOptions, type StaticProductManifest } from './core.js';
6
+ import { type OpenReleaseOptions } from './release.js';
7
+ export { isMapGeographyModel };
8
+ export type { MapGeographyModel, StaticDomainProductClient };
9
+ export type StaticMapProductClient = Omit<StaticDomainProductClient, 'manifest'> & {
10
+ readonly manifest: StaticProductManifest & {
11
+ readonly productId: 'maps';
12
+ };
13
+ };
14
+ /** Open a pinned Maps domain product directly. */
15
+ export declare function openMapProduct(options: OpenProductOptions): Promise<StaticMapProductClient>;
16
+ export declare function isMapProductClient(product: StaticDomainProductClient): product is StaticMapProductClient;
17
+ /** Options for the one-shot, manifest-pinned geography model read. */
18
+ export type GetMapGeographyOptions = OpenReleaseOptions & {
19
+ readonly buildId: string;
20
+ };
21
+ /** Read the selected build's digest-verified Map geography model. */
22
+ export declare function getMapGeography(options: GetMapGeographyOptions): Promise<MapGeographyModel>;
package/client/maps.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@mapletip/data/maps` — verified Map geography and Map catalog reads.
3
+ */
4
+ import { isMapGeographyModel, } from './static-domains.js';
5
+ import { UnsupportedProductError, } from './core.js';
6
+ import { openStaticDomainProduct } from './static-domains.js';
7
+ import { openRelease } from './release.js';
8
+ export { isMapGeographyModel };
9
+ /** Open a pinned Maps domain product directly. */
10
+ export async function openMapProduct(options) {
11
+ const product = await openStaticDomainProduct(options);
12
+ if (product.manifest.productId !== 'maps') {
13
+ throw new UnsupportedProductError(product.manifest.productId);
14
+ }
15
+ return product;
16
+ }
17
+ export function isMapProductClient(product) {
18
+ return product.manifest.productId === 'maps';
19
+ }
20
+ /** Read the selected build's digest-verified Map geography model. */
21
+ export async function getMapGeography(options) {
22
+ const release = await openRelease(options);
23
+ const maps = await release.openDomainBuild(options.buildId, 'maps');
24
+ return maps.loadGeography({ signal: options.signal });
25
+ }
@@ -9,6 +9,14 @@
9
9
  import type { MonsterCatalog } from '../generated/monster-catalog.d.ts';
10
10
  import type { MonsterCatalogV2 } from '../generated/monster-catalog-2.d.ts';
11
11
  import type { MonsterCatalogV2_1 } from '../generated/monster-catalog-2-1.d.ts';
12
+ import type { MonsterCatalogV2_2 } from '../generated/monster-catalog-2-2.d.ts';
13
+ import type { MonsterCatalogV2_3 } from '../generated/monster-catalog-2-3.d.ts';
14
+ import type { MonsterCatalogV2_4 } from '../generated/monster-catalog-2-4.d.ts';
15
+ import type { MonsterDropBrowseV1 } from '../generated/monster-drop-browse.d.ts';
16
+ import type { MonsterDropBrowseItemV1_1, MonsterDropBrowseV1_1 } from '../generated/monster-drop-browse-1-1.d.ts';
17
+ import type { CatalogFact as CatalogFact2 } from '../generated/contract-2-catalog-fact.d.ts';
18
+ import type { MonsterLocationBrowseV1 } from '../generated/monster-location-browse.d.ts';
19
+ import type { MonsterLocationBrowseV1_1 } from '../generated/monster-location-browse-1-1.d.ts';
12
20
  import type { MonsterDetailRecord, PageDescriptor } from '../generated/monster-detail.d.ts';
13
21
  import type { MonsterDetailRecordV2 } from '../generated/monster-detail-2.d.ts';
14
22
  import type { MonsterEvidencePage } from '../generated/monster-evidence-pack.d.ts';
@@ -17,15 +25,25 @@ import type { MonsterLocationOccurrence } from '../generated/monster-relationshi
17
25
  import type { MonsterCoreRelationshipPackV2 } from '../generated/monster-core-relationship-pack-2.d.ts';
18
26
  import { type OpenProductOptions, type OperationOptions, type StaticProductManifest, type VerifiedProductContext } from './core.js';
19
27
  /**
20
- * A verified Monster catalog. Contract 2.1 records carry `sprite`, a Media content ID
21
- * fact for the monster's representative image; 2.0 and 1.x records have none.
28
+ * A verified Monster catalog. Contracts 2.1 through 2.4 carry the Media content ID
29
+ * fact for a monster's representative image; 2.0 and 1.x records have none.
22
30
  */
23
- export type StaticMonsterCatalog = MonsterCatalog | MonsterCatalogV2 | MonsterCatalogV2_1;
31
+ export type StaticMonsterCatalog = MonsterCatalog | MonsterCatalogV2 | MonsterCatalogV2_1 | MonsterCatalogV2_2 | MonsterCatalogV2_3 | MonsterCatalogV2_4;
32
+ type StaticMonsterDropBrowseItemV1_1 = Omit<MonsterDropBrowseItemV1_1, 'icon'> & {
33
+ readonly icon: CatalogFact2<string>;
34
+ };
35
+ export type StaticMonsterDropBrowseV1_1 = Omit<MonsterDropBrowseV1_1, 'items' | 'itemIcons'> & {
36
+ readonly items: StaticMonsterDropBrowseItemV1_1[];
37
+ };
38
+ export type StaticMonsterDropBrowse = MonsterDropBrowseV1 | StaticMonsterDropBrowseV1_1;
39
+ export type StaticMonsterLocationBrowse = MonsterLocationBrowseV1 | MonsterLocationBrowseV1_1;
24
40
  export type StaticMonsterDetail = MonsterDetailRecord | MonsterDetailRecordV2;
25
41
  export type StaticMonsterEvidencePage = MonsterEvidencePage | MonsterEvidencePageV2;
26
42
  export interface StaticProductClient {
27
43
  readonly manifest: StaticProductManifest;
28
44
  loadCatalog(options?: OperationOptions): Promise<StaticMonsterCatalog>;
45
+ loadDropBrowse(options?: OperationOptions): Promise<StaticMonsterDropBrowse>;
46
+ loadLocationBrowse(options?: OperationOptions): Promise<StaticMonsterLocationBrowse>;
29
47
  getDetail(key: string, options?: OperationOptions): Promise<StaticMonsterDetail>;
30
48
  getRelationshipPage(page: PageDescriptor, options?: OperationOptions): Promise<MonsterLocationOccurrence[]>;
31
49
  getEvidencePage(page: PageDescriptor, options?: OperationOptions): Promise<StaticMonsterEvidencePage>;
@@ -38,3 +56,4 @@ export declare function openMonsterProduct(options: OpenProductOptions): Promise
38
56
  export declare function isMonsterProductClient(client: {
39
57
  readonly manifest: StaticProductManifest;
40
58
  }): client is StaticProductClient;
59
+ export {};