@wfcd/relics 2.0.30 → 2.0.31

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 CHANGED
@@ -1,3 +1,10 @@
1
+ ## [2.0.31](https://github.com/WFCD/warframe-relic-data/compare/v2.0.30...v2.0.31) (2026-08-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **#412:** uniqueName stamping on relic drops ([#422](https://github.com/WFCD/warframe-relic-data/issues/422)) ([df55cfd](https://github.com/WFCD/warframe-relic-data/commit/df55cfd58e29c3d0504b6201e5611439cfce7f3e)), closes [#412](https://github.com/WFCD/warframe-relic-data/issues/412)
7
+
1
8
  ## [2.0.30](https://github.com/WFCD/warframe-relic-data/compare/v2.0.29...v2.0.30) (2026-08-04)
2
9
 
3
10
 
package/dist/Build.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_VersionManager = require("./VersionManager-ZP_iui8x.cjs");
1
+ const require_VersionManager = require("./VersionManager-CDrWT0KK.cjs");
2
2
  //#region src/Build.ts
3
3
  /**
4
4
  * Entrypoint for the build process.
package/dist/Build.d.cts CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {}
package/dist/Build.d.mts CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {}
package/dist/Build.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./rolldown-runtime-C0LytTxp.mjs";
2
- import { n as Generator, r as Logger_default, t as VersionManager } from "./VersionManager-W7wuuPML.mjs";
2
+ import { n as Generator, r as Logger_default, t as VersionManager } from "./VersionManager-BckuXhbh.mjs";
3
3
  //#region src/Build.ts
4
4
  /**
5
5
  * Entrypoint for the build process.
@@ -1,68 +1,112 @@
1
1
  import "./rolldown-runtime-C0LytTxp.mjs";
2
+ import fetch from "node-fetch";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
- import fetch from "node-fetch";
5
5
  import * as console from "node:console";
6
6
  //#region src/Config.ts
7
7
  const Config = {
8
- warframeRelicDropUrl: "https://drops.warframestat.us/data/relics.json",
9
- warframeMarketItemUrl: "https://api.warframe.market/v2/items",
8
+ warframeItemByNameUrl: "https://api.warframestat.us/items/",
9
+ warframeItemsSparseUrl: "https://api.warframestat.us/items/?only=name,uniqueName",
10
10
  warframeItemsUrl: "https://api.warframestat.us/items/search/Relics?by=category",
11
+ warframeMarketItemUrl: "https://api.warframe.market/v2/items",
12
+ warframePatchlogsUrl: "https://raw.githubusercontent.com/WFCD/warframe-patchlogs/master/data/patchlogs.json",
11
13
  warframeRelicDropInfoUrl: "https://drops.warframestat.us/data/info.json",
12
- warframePatchlogsUrl: "https://raw.githubusercontent.com/WFCD/warframe-patchlogs/master/data/patchlogs.json"
14
+ warframeRelicDropUrl: "https://drops.warframestat.us/data/relics.json"
13
15
  };
14
16
  //#endregion
17
+ //#region src/itemUniqueName.ts
18
+ const QTY_PREFIX = /^\d+X\s+/i;
19
+ const BLUEPRINT_SUFFIX = / Blueprint$/i;
20
+ /**
21
+ * Index sparse warframestat items by lowercase name, preferring higher-ranked uniqueNames.
22
+ * @param {Array<WFCDSparseItem>} items Sparse item list from warframestat
23
+ * @returns {Map<string, string>} Lowercase name → uniqueName
24
+ */
25
+ function buildItemUniqueNameIndex(items) {
26
+ const index = /* @__PURE__ */ new Map();
27
+ items.forEach((item) => {
28
+ if (!(item === null || item === void 0 ? void 0 : item.name) || !(item === null || item === void 0 ? void 0 : item.uniqueName)) return;
29
+ const key = item.name.toLowerCase();
30
+ const existing = index.get(key);
31
+ if (!existing || uniqueNameRank(item.uniqueName) > uniqueNameRank(existing)) index.set(key, item.uniqueName);
32
+ });
33
+ return index;
34
+ }
35
+ /**
36
+ * Strip drop quantity prefixes (e.g. "1200X Kuva" → "Kuva").
37
+ * @param {string} itemName Raw drop item name
38
+ * @returns {string} Name without quantity prefix
39
+ */
40
+ function normalizeRewardName(itemName) {
41
+ return itemName.replace(QTY_PREFIX, "").trim();
42
+ }
43
+ /**
44
+ * Resolve reward uniqueName from WFM gameRef or sparse name index.
45
+ * @param {string} itemName Raw drop item name
46
+ * @param {string|undefined} gameRef WFM gameRef when matched
47
+ * @param {Map<string, string>} itemUniqueNames Sparse (+ Blueprint) index
48
+ * @returns {string} Lotus uniqueName or empty string
49
+ */
50
+ function resolveRewardUniqueName(itemName, gameRef, itemUniqueNames) {
51
+ if (gameRef) return gameRef;
52
+ return itemUniqueNames.get(normalizeRewardName(itemName).toLowerCase()) ?? "";
53
+ }
54
+ /**
55
+ * Prefer in-world item/recipe paths over StoreItems, enemies, and other duplicates.
56
+ * @param {string} uniqueName Lotus path
57
+ * @returns {number} Higher is preferred
58
+ */
59
+ function uniqueNameRank(uniqueName) {
60
+ if (uniqueName.includes("/Enemies/")) return 0;
61
+ if (uniqueName.startsWith("/Lotus/Types/Items/") || uniqueName.startsWith("/Lotus/Types/Recipes/")) return 3;
62
+ if (uniqueName.startsWith("/Lotus/Types/")) return 2;
63
+ if (uniqueName.startsWith("/Lotus/StoreItems/")) return 1;
64
+ return 0;
65
+ }
66
+ //#endregion
15
67
  //#region src/Logger.ts
16
68
  const fromString = (logLevelIsh) => {
17
69
  switch (logLevelIsh === null || logLevelIsh === void 0 ? void 0 : logLevelIsh.toLowerCase()) {
70
+ case "bad":
71
+ case "error": return 0;
72
+ case "debug": return 2;
18
73
  case "fatal": return -1;
19
- case "error":
20
- case "bad": return 0;
21
74
  case "info":
22
75
  case "log": return 1;
23
- case "debug": return 2;
24
76
  default: return -1;
25
77
  }
26
78
  };
27
79
  var Logger = class {
28
- logLevel = fromString(process.env.LOG_LEVEL || "fatal");
29
- log(message) {
30
- if (this.logLevel >= 1) console.log(message);
80
+ logLevel = fromString(process.env.LOG_LEVEL ?? "fatal");
81
+ debug(message) {
82
+ if (this.logLevel === 2) console.debug(message);
31
83
  }
32
84
  error(message) {
33
85
  if (this.logLevel >= 0) console.error(message);
34
86
  }
35
- debug(message) {
36
- if (this.logLevel === 2) console.debug(message);
37
- }
38
87
  fatal(message) {
39
88
  if (this.logLevel >= -1) {
40
89
  console.error(`FATAL: ${message}`);
41
90
  throw new Error(message);
42
91
  }
43
92
  }
93
+ log(message) {
94
+ if (this.logLevel >= 1) console.log(message);
95
+ }
44
96
  };
45
97
  var Logger_default = new Logger();
46
98
  //#endregion
47
99
  //#region src/Generator.ts
48
100
  var Generator = class {
101
+ /** Lowercase item name → uniqueName from sparse warframestat index (+ Blueprint follow-ups) */
102
+ itemUniqueNames;
103
+ relics;
49
104
  relicsRaw;
50
105
  wfcdItems;
51
106
  wfmItems;
52
- relics;
53
107
  constructor() {
54
108
  this.relics = [];
55
- }
56
- /**
57
- * Main function to fetch and generate the relic data
58
- * @returns {Promise<Array<TitaniaRelic>>} The Relics data array
59
- */
60
- async generate() {
61
- Logger_default.log("Starting Generation");
62
- await this.fetchRawData();
63
- this.filterWFCDRelics();
64
- this.generateTitaniaRelics();
65
- return this.relics;
109
+ this.itemUniqueNames = /* @__PURE__ */ new Map();
66
110
  }
67
111
  /**
68
112
  * Fetches all required data from WFCD and WFM.
@@ -87,6 +131,25 @@ var Generator = class {
87
131
  return;
88
132
  }
89
133
  this.wfcdItems = await wfcdItemRequest.json();
134
+ const sparseRequest = await fetch(Config.warframeItemsSparseUrl);
135
+ if (!sparseRequest.ok) {
136
+ Logger_default.error("Failed to fetch sparse items from WFCD!");
137
+ return;
138
+ }
139
+ const sparseItems = await sparseRequest.json();
140
+ this.indexSparseItems(sparseItems);
141
+ }
142
+ /**
143
+ * Main function to fetch and generate the relic data
144
+ * @returns {Promise<Array<TitaniaRelic>>} The Relics data array
145
+ */
146
+ async generate() {
147
+ Logger_default.log("Starting Generation");
148
+ await this.fetchRawData();
149
+ this.filterWFCDRelics();
150
+ await this.resolveSpecialRewardUniqueNames();
151
+ this.generateTitaniaRelics();
152
+ return this.relics;
90
153
  }
91
154
  /**
92
155
  * Generates the relic data
@@ -123,17 +186,46 @@ var Generator = class {
123
186
  }
124
187
  }
125
188
  /**
189
+ * Fetch a single warframestat item by name with components for Blueprint resolution.
190
+ * @param {string} name Item display name
191
+ * @returns {Promise<WFCDSparseItem|undefined>} Item or undefined on failure
192
+ */
193
+ async fetchItemByName(name) {
194
+ const url = `${Config.warframeItemByNameUrl}${encodeURIComponent(name)}?only=name,uniqueName,components`;
195
+ const response = await fetch(url);
196
+ if (!response.ok) {
197
+ Logger_default.debug(`Failed to fetch item by name: ${name} (${response.status})`);
198
+ return;
199
+ }
200
+ const data = await response.json();
201
+ if ("error" in data && data.error) {
202
+ Logger_default.debug(`No item result for: ${name}`);
203
+ return;
204
+ }
205
+ return data;
206
+ }
207
+ /**
208
+ * Filters WFCD's relic data to only include Intact variants, since we just need the base.
209
+ */
210
+ filterWFCDRelics() {
211
+ var _this$relicsRaw;
212
+ var _this$relicsRaw2;
213
+ var _this$relicsRaw3;
214
+ const before = ((_this$relicsRaw = this.relicsRaw) === null || _this$relicsRaw === void 0 ? void 0 : _this$relicsRaw.length) ?? 0;
215
+ this.relicsRaw = (_this$relicsRaw2 = this.relicsRaw) === null || _this$relicsRaw2 === void 0 ? void 0 : _this$relicsRaw2.filter((x) => x.state === "Intact");
216
+ Logger_default.log(`Filtered relics to intact variants. Before: ${before} After: ${((_this$relicsRaw3 = this.relicsRaw) === null || _this$relicsRaw3 === void 0 ? void 0 : _this$relicsRaw3.length) ?? 0}`);
217
+ }
218
+ /**
126
219
  * Generates a single relic from all available data
127
220
  * @param {WFCDRelic} rawRelic relic to pull Titania data from
128
221
  * @returns {TitaniaRelicReward}
129
222
  */
130
223
  generateTitaniaRelic(rawRelic) {
131
- var _this$wfcdItems2;
224
+ var _this$wfcdItems;
132
225
  var _this$wfmItems2;
133
226
  const name = `${rawRelic.tier} ${rawRelic.relicName}`;
134
227
  const rewards = rawRelic.rewards.map((rawReward) => {
135
228
  var _this$wfmItems;
136
- var _this$wfcdItems;
137
229
  const { chance } = rawReward;
138
230
  const { rarity } = rawReward;
139
231
  const wfmInfo = (_this$wfmItems = this.wfmItems) === null || _this$wfmItems === void 0 ? void 0 : _this$wfmItems.data.find((x) => {
@@ -148,7 +240,7 @@ var Generator = class {
148
240
  if (!(wfmInfo || isSpecial)) Logger_default.debug(`Failed to find wfm item for ${rawReward.itemName}`);
149
241
  const item = {
150
242
  name: rawReward.itemName,
151
- uniqueName: ((_this$wfcdItems = this.wfcdItems) === null || _this$wfcdItems === void 0 || (_this$wfcdItems = _this$wfcdItems.find((x) => x.name.toLowerCase() === `${name.trim()} Intact`.toLowerCase())) === null || _this$wfcdItems === void 0 ? void 0 : _this$wfcdItems.uniqueName) || "",
243
+ uniqueName: resolveRewardUniqueName(rawReward.itemName, wfmInfo === null || wfmInfo === void 0 ? void 0 : wfmInfo.gameRef, this.itemUniqueNames),
152
244
  warframeMarket: void 0
153
245
  };
154
246
  if (wfmInfo) item.warframeMarket = {
@@ -156,19 +248,19 @@ var Generator = class {
156
248
  urlName: wfmInfo.slug
157
249
  };
158
250
  return {
159
- rarity,
160
251
  chance,
161
- item
252
+ item,
253
+ rarity
162
254
  };
163
255
  });
164
256
  let drops = [];
165
- const wfcdItem = (_this$wfcdItems2 = this.wfcdItems) === null || _this$wfcdItems2 === void 0 ? void 0 : _this$wfcdItems2.find((x) => x.name.toLowerCase() === `${name.trim()} Intact`.toLowerCase());
257
+ const wfcdItem = (_this$wfcdItems = this.wfcdItems) === null || _this$wfcdItems === void 0 ? void 0 : _this$wfcdItems.find((x) => x.name.toLowerCase() === `${name.trim()} Intact`.toLowerCase());
166
258
  if (!wfcdItem) Logger_default.error(`Failed to get WFCD item for relic: ${name}`);
167
- if (wfcdItem && wfcdItem.drops) drops = wfcdItem.drops.map((rawDrop) => {
259
+ if (wfcdItem === null || wfcdItem === void 0 ? void 0 : wfcdItem.drops) drops = wfcdItem.drops.map((rawDrop) => {
168
260
  return {
169
- rarity: rawDrop.rarity,
170
261
  chance: rawDrop.chance,
171
- location: rawDrop.location
262
+ location: rawDrop.location,
263
+ rarity: rawDrop.rarity
172
264
  };
173
265
  });
174
266
  const wfm = (_this$wfmItems2 = this.wfmItems) === null || _this$wfmItems2 === void 0 ? void 0 : _this$wfmItems2.data.find((x) => {
@@ -176,35 +268,71 @@ var Generator = class {
176
268
  });
177
269
  if (!wfm) Logger_default.error(`Failed to get relic item from wfm: ${name}`);
178
270
  return {
271
+ locations: drops,
179
272
  name,
180
273
  rewards,
181
- locations: drops,
182
- uniqueName: (wfcdItem === null || wfcdItem === void 0 ? void 0 : wfcdItem.uniqueName) || "",
274
+ uniqueName: (wfcdItem === null || wfcdItem === void 0 ? void 0 : wfcdItem.uniqueName) ?? "",
183
275
  vaultInfo: { vaulted: drops.length === 0 },
184
- ...(wfm === null || wfm === void 0 ? void 0 : wfm.id) && (wfm === null || wfm === void 0 ? void 0 : wfm.slug) && { warframeMarket: {
185
- id: wfm === null || wfm === void 0 ? void 0 : wfm.id,
186
- urlName: wfm === null || wfm === void 0 ? void 0 : wfm.slug
276
+ ...(wfm === null || wfm === void 0 ? void 0 : wfm.id) && wfm.slug && { warframeMarket: {
277
+ id: wfm.id,
278
+ urlName: wfm.slug
187
279
  } }
188
280
  };
189
281
  }
190
282
  /**
191
- * Filters WFCD's relic data to only include Intact variants, since we just need the base.
283
+ * Index sparse warframestat items by lowercase name, preferring /Lotus/Types/ paths.
284
+ * @param {Array<WFCDSparseItem>} items Sparse item list from warframestat
192
285
  */
193
- filterWFCDRelics() {
194
- var _this$relicsRaw;
195
- var _this$relicsRaw2;
196
- var _this$relicsRaw3;
197
- const before = ((_this$relicsRaw = this.relicsRaw) === null || _this$relicsRaw === void 0 ? void 0 : _this$relicsRaw.length) || 0;
198
- this.relicsRaw = (_this$relicsRaw2 = this.relicsRaw) === null || _this$relicsRaw2 === void 0 ? void 0 : _this$relicsRaw2.filter((x) => x.state === "Intact");
199
- Logger_default.log(`Filtered relics to intact variants. Before: ${before} After: ${((_this$relicsRaw3 = this.relicsRaw) === null || _this$relicsRaw3 === void 0 ? void 0 : _this$relicsRaw3.length) || 0}`);
286
+ indexSparseItems(items) {
287
+ this.itemUniqueNames = buildItemUniqueNameIndex(items);
288
+ Logger_default.debug(`Indexed ${this.itemUniqueNames.size} sparse item uniqueNames`);
289
+ }
290
+ /**
291
+ * For reward names missing WFM, resolve uniqueNames from sparse index.
292
+ * Blueprint-suffixed names need a parent item fetch for the Blueprint component.
293
+ */
294
+ async resolveSpecialRewardUniqueNames() {
295
+ if (!this.relicsRaw || !this.wfmItems) return;
296
+ const specialNames = /* @__PURE__ */ new Set();
297
+ this.relicsRaw.forEach((relic) => {
298
+ relic.rewards.forEach((reward) => {
299
+ var _this$wfmItems3;
300
+ if (!((_this$wfmItems3 = this.wfmItems) === null || _this$wfmItems3 === void 0 ? void 0 : _this$wfmItems3.data.some((x) => {
301
+ return x.i18n.en.name.toLowerCase() === reward.itemName.toLowerCase();
302
+ }))) specialNames.add(normalizeRewardName(reward.itemName));
303
+ });
304
+ });
305
+ const blueprintParents = /* @__PURE__ */ new Map();
306
+ Array.from(specialNames).forEach((name) => {
307
+ const key = name.toLowerCase();
308
+ if (this.itemUniqueNames.has(key)) return;
309
+ if (!BLUEPRINT_SUFFIX.test(name)) {
310
+ Logger_default.debug(`No uniqueName for special reward: ${name}`);
311
+ return;
312
+ }
313
+ const parent = name.replace(BLUEPRINT_SUFFIX, "").trim();
314
+ if (parent) blueprintParents.set(name, parent);
315
+ });
316
+ const parentCache = /* @__PURE__ */ new Map();
317
+ for (const [blueprintName, parent] of blueprintParents.entries()) {
318
+ var _parentItem$component;
319
+ let parentItem = parentCache.get(parent);
320
+ if (!parentCache.has(parent)) {
321
+ parentItem = await this.fetchItemByName(parent);
322
+ parentCache.set(parent, parentItem);
323
+ }
324
+ const blueprint = parentItem === null || parentItem === void 0 || (_parentItem$component = parentItem.components) === null || _parentItem$component === void 0 ? void 0 : _parentItem$component.find((c) => c.name.toLowerCase() === "blueprint");
325
+ if (blueprint === null || blueprint === void 0 ? void 0 : blueprint.uniqueName) this.itemUniqueNames.set(blueprintName.toLowerCase(), blueprint.uniqueName);
326
+ else Logger_default.debug(`Failed to resolve Blueprint uniqueName for: ${blueprintName}`);
327
+ }
200
328
  }
201
329
  };
202
330
  //#endregion
203
331
  //#region src/VersionManager.ts
204
332
  var VersionManager = class {
333
+ hashPath;
205
334
  versionPath;
206
335
  versionRawPath;
207
- hashPath;
208
336
  /**
209
337
  * Creates a new VersionManager instance.
210
338
  * @param {string} dataDir Folder to write version information to.
@@ -226,7 +354,7 @@ var VersionManager = class {
226
354
  try {
227
355
  await fs.access(this.hashPath);
228
356
  return JSON.parse(await fs.readFile(this.hashPath, "utf-8")).hash !== info.hash;
229
- } catch (ex) {
357
+ } catch {
230
358
  return true;
231
359
  }
232
360
  }
@@ -249,12 +377,12 @@ var VersionManager = class {
249
377
  }
250
378
  const hashInfo = await hashReq.json();
251
379
  const versionInfo = {
252
- version,
253
- title: patchlogs[0].name
380
+ title: patchlogs[0].name,
381
+ version
254
382
  };
255
383
  const hashFile = {
256
- hash: hashInfo.hash,
257
384
  deUpdated: hashInfo.modified,
385
+ hash: hashInfo.hash,
258
386
  timestamp
259
387
  };
260
388
  await fs.writeFile(this.hashPath, JSON.stringify(hashFile, void 0, 2), "utf-8");