-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathGenerator.ts
More file actions
282 lines (249 loc) Β· 9.49 KB
/
Copy pathGenerator.ts
File metadata and controls
282 lines (249 loc) Β· 9.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import fs from 'node:fs/promises';
import path from 'node:path';
import Config from './Config';
import {
BLUEPRINT_SUFFIX,
buildItemUniqueNameIndex,
normalizeRewardName,
resolveRewardUniqueName,
} from './itemUniqueName';
import logger from './Logger';
import {
Rarity,
TitaniaRelic,
TitaniaRelicLocation,
TitaniaRelicRewardItem,
WarframeMarketRoot,
WFCDItem,
WFCDRelic,
WFCDSparseItem,
} from './Types';
export class Generator {
/** Lowercase item name β uniqueName from sparse warframestat index (+ Blueprint follow-ups) */
itemUniqueNames: Map<string, string>;
relics: TitaniaRelic[];
relicsRaw: undefined | WFCDRelic[];
wfcdItems: undefined | WFCDItem[];
wfmItems: undefined | WarframeMarketRoot;
constructor() {
this.relics = [];
this.itemUniqueNames = new Map();
}
/**
* Fetches all required data from WFCD and WFM.
*/
public async fetchRawData(): Promise<void> {
const relicRequest = await fetch(Config.warframeRelicDropUrl);
if (!relicRequest.ok) {
logger.error('Failed to fetch Warframe relics from WFCD!');
return;
}
this.relicsRaw = ((await relicRequest.json()) as { relics: WFCDRelic[] })?.relics;
const wfmRequest = await fetch(Config.warframeMarketItemUrl);
if (!wfmRequest.ok) {
logger.error('Failed to fetch items from WFM!');
return;
}
this.wfmItems = (await wfmRequest.json()) as WarframeMarketRoot;
const wfcdItemRequest = await fetch(Config.warframeItemsUrl);
if (!wfcdItemRequest.ok) {
logger.error('Failed to fetch items from WFCD! ');
return;
}
this.wfcdItems = (await wfcdItemRequest.json()) as WFCDItem[];
const sparseRequest = await fetch(Config.warframeItemsSparseUrl);
if (!sparseRequest.ok) {
logger.error('Failed to fetch sparse items from WFCD!');
return;
}
const sparseItems = (await sparseRequest.json()) as WFCDSparseItem[];
this.indexSparseItems(sparseItems);
}
/**
* Main function to fetch and generate the relic data
* @returns {Promise<Array<TitaniaRelic>>} The Relics data array
*/
public async generate(): Promise<TitaniaRelic[]> {
logger.log('Starting Generation');
await this.fetchRawData();
this.filterWFCDRelics();
await this.resolveSpecialRewardUniqueNames();
this.generateTitaniaRelics();
return this.relics;
}
/**
* Generates the relic data
* uses WFCD/warframe-drop-data to check what relics exist,
* and adds information from WFCD/warframe-items and WFM
*/
public generateTitaniaRelics(): void {
if (typeof this.relicsRaw === 'undefined' || typeof this.wfmItems === 'undefined') {
logger.log('Failed to load relics/item data');
return;
}
const { length } = this.relicsRaw;
for (let i = 0; i < length; i += 1) {
const rawRelic = this.relicsRaw[i];
logger.debug(`[${i + 1}/${length}] ${rawRelic.tier} ${rawRelic.relicName}`);
const relic = this.generateTitaniaRelic(rawRelic);
this.relics.push(relic);
}
logger.debug(`Finished parsing ${this.relics.length} relics`);
}
/**
* Writes the fully generated data to disk.
* @param {string} dataDir Directory to store the relic data in. Default: ../data/
* @param {string} fileName Filename base ex: "Relics" becomes "Relics.json" and "Relics.min.json". Default: "Relics"
* @param {boolean} generateMin True if a minified json should be generated too. Default: true
*/
public async writeData(dataDir?: string, fileName?: string, generateMin = true) {
const DataDir = dataDir ?? path.join(__dirname, '..', 'data');
const RelicPath = fileName ? path.join(DataDir, `${fileName}.json`) : path.join(DataDir, 'Relics.json');
await fs.writeFile(RelicPath, JSON.stringify(this.relics, undefined, 4));
if (generateMin) {
const RelicMinPath = fileName
? path.join(DataDir, `${fileName}.min.json`)
: path.join(DataDir, 'Relics.min.json');
await fs.writeFile(RelicMinPath, JSON.stringify(this.relics));
}
}
/**
* Fetch a single warframestat item by name with components for Blueprint resolution.
* @param {string} name Item display name
* @returns {Promise<WFCDSparseItem|undefined>} Item or undefined on failure
*/
private async fetchItemByName(name: string): Promise<undefined | WFCDSparseItem> {
const url = `${Config.warframeItemByNameUrl}${encodeURIComponent(name)}?only=name,uniqueName,components`;
const response = await fetch(url);
if (!response.ok) {
logger.debug(`Failed to fetch item by name: ${name} (${response.status})`);
return undefined;
}
const data = (await response.json()) as WFCDSparseItem | { error?: string };
if ('error' in data && data.error) {
logger.debug(`No item result for: ${name}`);
return undefined;
}
return data as WFCDSparseItem;
}
/**
* Filters WFCD's relic data to only include Intact variants, since we just need the base.
*/
private filterWFCDRelics() {
const before = this.relicsRaw?.length ?? 0;
this.relicsRaw = this.relicsRaw?.filter((x) => x.state === 'Intact');
logger.log(`Filtered relics to intact variants. Before: ${before} After: ${this.relicsRaw?.length ?? 0}`);
}
/**
* Generates a single relic from all available data
* @param {WFCDRelic} rawRelic relic to pull Titania data from
* @returns {TitaniaRelicReward}
*/
private generateTitaniaRelic(rawRelic: WFCDRelic): TitaniaRelic {
const name = `${rawRelic.tier} ${rawRelic.relicName}`;
const rewards = rawRelic.rewards.map((rawReward) => {
const { chance } = rawReward;
const { rarity } = rawReward;
const wfmInfo = this.wfmItems?.data.find((x) => {
return x.i18n.en.name.toLowerCase() === rawReward.itemName.toLowerCase();
});
const isSpecial = ['Forma', 'Kuva', 'Exilus', 'Riven'].find((x) =>
rawReward.itemName.toLowerCase().includes(x.toLowerCase()),
);
if (!(wfmInfo || isSpecial)) {
logger.debug(`Failed to find wfm item for ${rawReward.itemName}`);
}
const item: TitaniaRelicRewardItem = {
name: rawReward.itemName,
uniqueName: resolveRewardUniqueName(rawReward.itemName, wfmInfo?.gameRef, this.itemUniqueNames),
warframeMarket: undefined,
};
if (wfmInfo) {
item.warframeMarket = { id: wfmInfo.id, urlName: wfmInfo.slug };
}
return { chance, item, rarity };
});
let drops: TitaniaRelicLocation[] = [];
const wfcdItem = this.wfcdItems?.find((x) => x.name.toLowerCase() === `${name.trim()} Intact`.toLowerCase());
if (!wfcdItem) {
logger.error(`Failed to get WFCD item for relic: ${name}`);
}
if (wfcdItem?.drops) {
drops = wfcdItem.drops.map((rawDrop) => {
return { chance: rawDrop.chance, location: rawDrop.location, rarity: rawDrop.rarity as Rarity };
});
}
const wfm = this.wfmItems?.data.find((x) => {
return x.i18n.en.name.toLowerCase() === `${name.trim()} Relic`.toLowerCase();
});
if (!wfm) {
logger.error(`Failed to get relic item from wfm: ${name}`);
}
return {
locations: drops,
name,
rewards,
uniqueName: wfcdItem?.uniqueName ?? '',
vaultInfo: { vaulted: drops.length === 0 },
...(wfm?.id && wfm.slug && { warframeMarket: { id: wfm.id, urlName: wfm.slug } }),
};
}
/**
* Index sparse warframestat items by lowercase name, preferring /Lotus/Types/ paths.
* @param {Array<WFCDSparseItem>} items Sparse item list from warframestat
*/
private indexSparseItems(items: WFCDSparseItem[]): void {
this.itemUniqueNames = buildItemUniqueNameIndex(items);
logger.debug(`Indexed ${this.itemUniqueNames.size} sparse item uniqueNames`);
}
/**
* For reward names missing WFM, resolve uniqueNames from sparse index.
* Blueprint-suffixed names need a parent item fetch for the Blueprint component.
*/
private async resolveSpecialRewardUniqueNames(): Promise<void> {
if (!this.relicsRaw || !this.wfmItems) {
return;
}
const specialNames = new Set<string>();
this.relicsRaw.forEach((relic) => {
relic.rewards.forEach((reward) => {
const hasWfm = this.wfmItems?.data.some((x) => {
return x.i18n.en.name.toLowerCase() === reward.itemName.toLowerCase();
});
if (!hasWfm) {
specialNames.add(normalizeRewardName(reward.itemName));
}
});
});
const blueprintParents = new Map<string, string>();
Array.from(specialNames).forEach((name) => {
const key = name.toLowerCase();
if (this.itemUniqueNames.has(key)) {
return;
}
if (!BLUEPRINT_SUFFIX.test(name)) {
logger.debug(`No uniqueName for special reward: ${name}`);
return;
}
const parent = name.replace(BLUEPRINT_SUFFIX, '').trim();
if (parent) {
blueprintParents.set(name, parent);
}
});
const parentCache = new Map<string, undefined | WFCDSparseItem>();
for (const [blueprintName, parent] of blueprintParents.entries()) {
let parentItem = parentCache.get(parent);
if (!parentCache.has(parent)) {
parentItem = await this.fetchItemByName(parent);
parentCache.set(parent, parentItem);
}
const blueprint = parentItem?.components?.find((c) => c.name.toLowerCase() === 'blueprint');
if (blueprint?.uniqueName) {
this.itemUniqueNames.set(blueprintName.toLowerCase(), blueprint.uniqueName);
} else {
logger.debug(`Failed to resolve Blueprint uniqueName for: ${blueprintName}`);
}
}
}
}
export default Generator;