@omnigateway/pokemon 1.1.0 → 1.2.2
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/omni-plugin.json +1 -1
- package/package.json +1 -1
- package/server/index.js +1015 -815
- package/ui/index.js +304 -96
package/server/index.js
CHANGED
|
@@ -61,7 +61,8 @@ var ITEM_SPRITE_FILES = {
|
|
|
61
61
|
incense: "luck-incense",
|
|
62
62
|
lure: "honey",
|
|
63
63
|
mint: "mental-herb",
|
|
64
|
-
egg: "lucky-egg"
|
|
64
|
+
egg: "lucky-egg",
|
|
65
|
+
incubating: "mystery-egg"
|
|
65
66
|
};
|
|
66
67
|
var ITEM_SPRITE_NAMES = new Map(Object.entries(ITEM_SPRITE_FILES));
|
|
67
68
|
var FRESH_EGG_BASE_PRICE = 1e9;
|
|
@@ -85,909 +86,1094 @@ function hasAnimatedSprite(speciesId) {
|
|
|
85
86
|
return speciesId >= 1 && speciesId <= ANIMATED_SPECIES_MAX;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
//
|
|
89
|
-
var
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
89
|
+
// src/advance.ts
|
|
90
|
+
var MAX_TRANSITIONS_PER_ADVANCE = 64;
|
|
91
|
+
function advance(state, tokensTotal, now) {
|
|
92
|
+
const gained = Math.max(0, Math.trunc(tokensTotal) - state.consumedTotal);
|
|
93
|
+
const events = [];
|
|
94
|
+
let next = { ...state, consumedTotal: Math.trunc(tokensTotal) };
|
|
95
|
+
if (gained > 0) {
|
|
96
|
+
const active = next.active;
|
|
97
|
+
if (active === null) {
|
|
98
|
+
next = { ...next, eggUsage: next.eggUsage + gained };
|
|
99
|
+
} else if (!active.soothe) {
|
|
100
|
+
next = { ...next, active: { ...active, usedAtStage: active.usedAtStage + gained } };
|
|
101
|
+
} else {
|
|
102
|
+
const raw = active.soothedRaw + gained;
|
|
103
|
+
const owed = Math.floor(raw * SOOTHE_BONUS) - Math.floor(active.soothedRaw * SOOTHE_BONUS);
|
|
104
|
+
next = {
|
|
105
|
+
...next,
|
|
106
|
+
active: { ...active, soothedRaw: raw, usedAtStage: active.usedAtStage + gained + owed }
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (let step = 0;step < MAX_TRANSITIONS_PER_ADVANCE; step++) {
|
|
111
|
+
if (next.active === null) {
|
|
112
|
+
if (next.eggUsage < EGG_HATCH_THRESHOLD)
|
|
113
|
+
break;
|
|
114
|
+
if (next.pendingHatch === null)
|
|
115
|
+
break;
|
|
116
|
+
const hatch = next.pendingHatch;
|
|
117
|
+
const active = {
|
|
118
|
+
baseId: hatch.path[0] ?? hatch.speciesId,
|
|
119
|
+
plannedPath: hatch.path,
|
|
120
|
+
stageIndex: 0,
|
|
121
|
+
stageTimes: [now],
|
|
122
|
+
usedAtStage: next.eggUsage - EGG_HATCH_THRESHOLD,
|
|
123
|
+
rarity: hatch.rarity,
|
|
124
|
+
isShiny: hatch.isShiny,
|
|
125
|
+
nature: hatch.nature,
|
|
126
|
+
dittoDisguise: hatch.ditto ? hatch.speciesId : null,
|
|
127
|
+
dittoRevealed: false,
|
|
128
|
+
everstone: false,
|
|
129
|
+
soothe: false,
|
|
130
|
+
soothedRaw: 0
|
|
131
|
+
};
|
|
132
|
+
events.push({
|
|
133
|
+
kind: "hatched",
|
|
134
|
+
speciesId: hatch.speciesId,
|
|
135
|
+
isShiny: active.isShiny,
|
|
136
|
+
ditto: active.dittoDisguise !== null
|
|
137
|
+
});
|
|
138
|
+
next = { ...next, active, eggUsage: 0, eggTier: null, pendingHatch: null };
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const mon = next.active;
|
|
142
|
+
if (mon.everstone)
|
|
143
|
+
break;
|
|
144
|
+
const needed = phaseThreshold(mon.rarity, mon.plannedPath.length, mon.stageIndex);
|
|
145
|
+
if (mon.usedAtStage < needed)
|
|
146
|
+
break;
|
|
147
|
+
const excess = mon.usedAtStage - needed;
|
|
148
|
+
if (mon.dittoDisguise !== null && !mon.dittoRevealed) {
|
|
149
|
+
if (next.pendingReveal === null)
|
|
150
|
+
break;
|
|
151
|
+
const reveal = next.pendingReveal;
|
|
152
|
+
events.push({
|
|
153
|
+
kind: "revealed",
|
|
154
|
+
disguisedAs: mon.plannedPath[mon.stageIndex] ?? mon.baseId,
|
|
155
|
+
speciesId: reveal.path[0]
|
|
156
|
+
});
|
|
157
|
+
next = {
|
|
158
|
+
...next,
|
|
159
|
+
active: {
|
|
160
|
+
...mon,
|
|
161
|
+
baseId: reveal.path[0],
|
|
162
|
+
plannedPath: reveal.path,
|
|
163
|
+
stageIndex: 0,
|
|
164
|
+
stageTimes: [now],
|
|
165
|
+
usedAtStage: excess,
|
|
166
|
+
rarity: reveal.rarity,
|
|
167
|
+
dittoRevealed: true
|
|
168
|
+
},
|
|
169
|
+
pendingReveal: null
|
|
170
|
+
};
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (mon.stageIndex < mon.plannedPath.length - 1) {
|
|
174
|
+
events.push({
|
|
175
|
+
kind: "evolved",
|
|
176
|
+
from: mon.plannedPath[mon.stageIndex],
|
|
177
|
+
to: mon.plannedPath[mon.stageIndex + 1]
|
|
178
|
+
});
|
|
179
|
+
next = {
|
|
180
|
+
...next,
|
|
181
|
+
active: {
|
|
182
|
+
...mon,
|
|
183
|
+
stageIndex: mon.stageIndex + 1,
|
|
184
|
+
usedAtStage: excess,
|
|
185
|
+
stageTimes: stampedAt(mon.stageTimes, mon.stageIndex + 1, now)
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
events.push({
|
|
191
|
+
kind: "graduated",
|
|
192
|
+
baseId: mon.baseId,
|
|
193
|
+
finalId: mon.plannedPath[mon.plannedPath.length - 1],
|
|
194
|
+
chainOrder: mon.plannedPath,
|
|
195
|
+
stageTimes: stampedAt(mon.stageTimes, mon.plannedPath.length - 1, now),
|
|
196
|
+
rarity: mon.rarity,
|
|
197
|
+
isShiny: mon.isShiny,
|
|
198
|
+
nature: mon.nature
|
|
199
|
+
});
|
|
200
|
+
next = { ...next, active: null, eggUsage: excess, eggTier: null, pendingHatch: null };
|
|
201
|
+
}
|
|
202
|
+
return { state: next, events };
|
|
105
203
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return { grant: false };
|
|
113
|
-
return { grant: true, count: grantSize(input.window), at: input.now };
|
|
204
|
+
function stampedAt(stageTimes, index, now) {
|
|
205
|
+
const placed = [...stageTimes];
|
|
206
|
+
while (placed.length <= index)
|
|
207
|
+
placed.push(null);
|
|
208
|
+
placed[index] ??= now;
|
|
209
|
+
return placed;
|
|
114
210
|
}
|
|
115
211
|
|
|
116
|
-
// src/
|
|
117
|
-
var
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
212
|
+
// src/roll.ts
|
|
213
|
+
var NATURES = [
|
|
214
|
+
"hardy",
|
|
215
|
+
"lonely",
|
|
216
|
+
"brave",
|
|
217
|
+
"adamant",
|
|
218
|
+
"naughty",
|
|
219
|
+
"bold",
|
|
220
|
+
"docile",
|
|
221
|
+
"relaxed",
|
|
222
|
+
"impish",
|
|
223
|
+
"lax",
|
|
224
|
+
"timid",
|
|
225
|
+
"hasty",
|
|
226
|
+
"serious",
|
|
227
|
+
"jolly",
|
|
228
|
+
"naive",
|
|
229
|
+
"modest",
|
|
230
|
+
"mild",
|
|
231
|
+
"quiet",
|
|
232
|
+
"bashful",
|
|
233
|
+
"rash",
|
|
234
|
+
"calm",
|
|
235
|
+
"gentle",
|
|
236
|
+
"sassy",
|
|
237
|
+
"careful",
|
|
238
|
+
"quirky"
|
|
239
|
+
];
|
|
240
|
+
function mulberry32(seed) {
|
|
241
|
+
let state = seed >>> 0;
|
|
242
|
+
return () => {
|
|
243
|
+
state = state + 1831565813 >>> 0;
|
|
244
|
+
let t = state;
|
|
245
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
246
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
247
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
248
|
+
};
|
|
130
249
|
}
|
|
131
|
-
|
|
132
|
-
|
|
250
|
+
var COLLECTED_WEIGHT = 0.25;
|
|
251
|
+
var FORM_WEIGHT = 0.6;
|
|
252
|
+
function roll(input) {
|
|
253
|
+
const random = mulberry32(input.seed);
|
|
254
|
+
const eligibleWith = (withLure) => input.candidates.filter((candidate) => {
|
|
255
|
+
if (!hasAnimatedSprite(candidate.id))
|
|
256
|
+
return false;
|
|
257
|
+
if (candidate.id === DITTO_SPECIES_ID)
|
|
258
|
+
return false;
|
|
259
|
+
if (input.excludeFinal != null && candidate.finalId === input.excludeFinal)
|
|
260
|
+
return false;
|
|
261
|
+
if (withLure && input.collectedFinals.has(candidate.finalId))
|
|
262
|
+
return false;
|
|
263
|
+
if (input.guarantee === null)
|
|
264
|
+
return true;
|
|
265
|
+
const rarity2 = rarityFromCaptureRate(candidate.captureRate, false, false);
|
|
266
|
+
return sortRank(rarity2) >= sortRank(input.guarantee);
|
|
267
|
+
});
|
|
268
|
+
const wanted = input.onlyUncollected === true;
|
|
269
|
+
const lured = wanted ? eligibleWith(true) : [];
|
|
270
|
+
const usedLure = wanted && lured.length > 0;
|
|
271
|
+
const eligible = usedLure ? lured : eligibleWith(false);
|
|
272
|
+
if (eligible.length === 0)
|
|
133
273
|
return null;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
274
|
+
const weights = eligible.map((candidate) => {
|
|
275
|
+
const base = Math.max(1, candidate.captureRate);
|
|
276
|
+
const collected = input.collectedFinals.has(candidate.finalId) ? base * COLLECTED_WEIGHT : base;
|
|
277
|
+
return input.preferLongLines === true ? collected * (1 + FORM_WEIGHT * (Math.max(1, candidate.forms) - 1)) : collected;
|
|
278
|
+
});
|
|
279
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
280
|
+
let target = random() * total;
|
|
281
|
+
let index = 0;
|
|
282
|
+
for (let i = 0;i < weights.length; i++) {
|
|
283
|
+
target -= weights[i];
|
|
284
|
+
if (target <= 0) {
|
|
285
|
+
index = i;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const chosen = eligible[index];
|
|
290
|
+
const shinyDenominator = input.hasShinyCharm ? ODDS.shinyWithCharm : ODDS.shiny;
|
|
291
|
+
const isShiny = random() < 1 / shinyDenominator;
|
|
292
|
+
const nature = NATURES[Math.floor(random() * NATURES.length)];
|
|
293
|
+
const rarity = rarityFromCaptureRate(chosen.captureRate, false, false);
|
|
294
|
+
const dittoEligible = rarity === "common" && chosen.forms >= 2;
|
|
295
|
+
const ditto = dittoEligible && random() < 1 / ODDS.dittoDisguise;
|
|
296
|
+
return { speciesId: chosen.id, isShiny, nature, ditto, usedLure };
|
|
138
297
|
}
|
|
139
|
-
|
|
140
|
-
|
|
298
|
+
|
|
299
|
+
// src/state.ts
|
|
300
|
+
function emptyInventory() {
|
|
301
|
+
const inventory = {};
|
|
302
|
+
for (const kind of ITEM_KINDS)
|
|
303
|
+
inventory[kind] = 0;
|
|
304
|
+
return inventory;
|
|
141
305
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
306
|
+
function freshState() {
|
|
307
|
+
return {
|
|
308
|
+
consumedTotal: 0,
|
|
309
|
+
active: null,
|
|
310
|
+
eggUsage: 0,
|
|
311
|
+
eggTier: null,
|
|
312
|
+
pendingHatch: null,
|
|
313
|
+
pendingReveal: null,
|
|
314
|
+
lure: false,
|
|
315
|
+
incense: false,
|
|
316
|
+
repel: null,
|
|
317
|
+
inventory: emptyInventory()
|
|
318
|
+
};
|
|
153
319
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
await deps.files.write(path, bytes);
|
|
157
|
-
} catch {}
|
|
320
|
+
function isRecord(value) {
|
|
321
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
158
322
|
}
|
|
159
|
-
|
|
160
|
-
|
|
323
|
+
function asInt(value, fallback) {
|
|
324
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
161
325
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const response = await deps.net(url);
|
|
165
|
-
if (!response.ok)
|
|
166
|
-
return null;
|
|
167
|
-
return JSON.parse(await response.text());
|
|
168
|
-
} catch {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
326
|
+
function asRarity(value) {
|
|
327
|
+
return RARITIES.includes(value) ? value : null;
|
|
171
328
|
}
|
|
172
|
-
|
|
329
|
+
function parseState(raw) {
|
|
330
|
+
let parsed;
|
|
173
331
|
try {
|
|
174
|
-
|
|
175
|
-
if (!response.ok)
|
|
176
|
-
return null;
|
|
177
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
178
|
-
return bytes.length === 0 ? null : bytes;
|
|
332
|
+
parsed = JSON.parse(raw);
|
|
179
333
|
} catch {
|
|
180
334
|
return null;
|
|
181
335
|
}
|
|
182
|
-
|
|
183
|
-
var CHAIN_URL_ID = /\/evolution-chain\/(\d+)\/?$/;
|
|
184
|
-
function chainIdFromUrl(url) {
|
|
185
|
-
if (typeof url !== "string")
|
|
186
|
-
return null;
|
|
187
|
-
const match = CHAIN_URL_ID.exec(url);
|
|
188
|
-
if (match === null)
|
|
189
|
-
return null;
|
|
190
|
-
const id = Number(match[1]);
|
|
191
|
-
if (!Number.isInteger(id) || id < 1 || id > MAX_EVOLUTION_CHAIN_ID)
|
|
192
|
-
return null;
|
|
193
|
-
return id;
|
|
194
|
-
}
|
|
195
|
-
var SPECIES_URL_ID = /\/pokemon-species\/(\d+)\/?$/;
|
|
196
|
-
function speciesIdFromUrl(url) {
|
|
197
|
-
if (typeof url !== "string")
|
|
198
|
-
return null;
|
|
199
|
-
const match = SPECIES_URL_ID.exec(url);
|
|
200
|
-
if (match === null)
|
|
201
|
-
return null;
|
|
202
|
-
const id = Number(match[1]);
|
|
203
|
-
return isFetchableSpeciesId(id) ? id : null;
|
|
204
|
-
}
|
|
205
|
-
function parseChainNode(raw) {
|
|
206
|
-
const node = asRecord(raw);
|
|
207
|
-
if (node === null)
|
|
208
|
-
return null;
|
|
209
|
-
const species = asRecord(node.species);
|
|
210
|
-
const id = speciesIdFromUrl(species?.url);
|
|
211
|
-
if (id === null)
|
|
336
|
+
if (!isRecord(parsed))
|
|
212
337
|
return null;
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
338
|
+
const inventory = emptyInventory();
|
|
339
|
+
const storedInventory = parsed.inventory;
|
|
340
|
+
if (isRecord(storedInventory)) {
|
|
341
|
+
for (const kind of ITEM_KINDS) {
|
|
342
|
+
inventory[kind] = Math.max(0, asInt(storedInventory[kind], 0));
|
|
343
|
+
}
|
|
219
344
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
345
|
+
let active = null;
|
|
346
|
+
const storedActive = parsed.active;
|
|
347
|
+
if (storedActive !== null && storedActive !== undefined) {
|
|
348
|
+
if (!isRecord(storedActive))
|
|
349
|
+
return null;
|
|
350
|
+
const rarity = asRarity(storedActive.rarity);
|
|
351
|
+
if (rarity === null)
|
|
352
|
+
return null;
|
|
353
|
+
const path = Array.isArray(storedActive.plannedPath) ? storedActive.plannedPath.filter((id) => typeof id === "number" && id > 0) : [];
|
|
354
|
+
if (path.length === 0)
|
|
355
|
+
return null;
|
|
356
|
+
const nature = NATURES.includes(storedActive.nature) ? storedActive.nature : null;
|
|
357
|
+
active = {
|
|
358
|
+
baseId: asInt(storedActive.baseId, path[0]),
|
|
359
|
+
plannedPath: path,
|
|
360
|
+
stageIndex: Math.min(Math.max(0, asInt(storedActive.stageIndex, 0)), path.length - 1),
|
|
361
|
+
stageTimes: Array.isArray(storedActive.stageTimes) ? storedActive.stageTimes.map((at) => typeof at === "number" && at >= 0 && Number.isFinite(at) ? at : null) : [],
|
|
362
|
+
usedAtStage: Math.max(0, asInt(storedActive.usedAtStage, 0)),
|
|
363
|
+
rarity,
|
|
364
|
+
isShiny: storedActive.isShiny === true,
|
|
365
|
+
nature: nature ?? "hardy",
|
|
366
|
+
dittoDisguise: typeof storedActive.dittoDisguise === "number" ? storedActive.dittoDisguise : null,
|
|
367
|
+
dittoRevealed: storedActive.dittoRevealed === true,
|
|
368
|
+
everstone: storedActive.everstone === true,
|
|
369
|
+
soothe: storedActive.soothe === true,
|
|
370
|
+
soothedRaw: Math.max(0, asInt(storedActive.soothedRaw, 0))
|
|
371
|
+
};
|
|
229
372
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
373
|
+
const storedPending = parsed.pendingHatch;
|
|
374
|
+
let pendingHatch = null;
|
|
375
|
+
if (isRecord(storedPending)) {
|
|
376
|
+
const rarity = asRarity(storedPending.rarity);
|
|
377
|
+
const path = Array.isArray(storedPending.path) ? storedPending.path.filter((id) => typeof id === "number" && id > 0) : [];
|
|
378
|
+
if (rarity !== null && path.length > 0) {
|
|
379
|
+
const nature = NATURES.includes(storedPending.nature) ? storedPending.nature : "hardy";
|
|
380
|
+
pendingHatch = {
|
|
381
|
+
speciesId: asInt(storedPending.speciesId, path[0]),
|
|
382
|
+
path,
|
|
383
|
+
rarity,
|
|
384
|
+
isShiny: storedPending.isShiny === true,
|
|
385
|
+
nature,
|
|
386
|
+
ditto: storedPending.ditto === true
|
|
387
|
+
};
|
|
388
|
+
}
|
|
239
389
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
390
|
+
const storedReveal = parsed.pendingReveal;
|
|
391
|
+
let pendingReveal = null;
|
|
392
|
+
if (isRecord(storedReveal)) {
|
|
393
|
+
const rarity = asRarity(storedReveal.rarity);
|
|
394
|
+
const path = Array.isArray(storedReveal.path) ? storedReveal.path.filter((id) => typeof id === "number" && id > 0) : [];
|
|
395
|
+
if (rarity !== null && path.length > 0)
|
|
396
|
+
pendingReveal = { path, rarity };
|
|
397
|
+
}
|
|
398
|
+
const storedConsumed = parsed.consumedTotal;
|
|
399
|
+
if (typeof storedConsumed !== "number" || !Number.isFinite(storedConsumed))
|
|
245
400
|
return null;
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
401
|
+
const eggTier = asRarity(parsed.eggTier);
|
|
402
|
+
return {
|
|
403
|
+
consumedTotal: Math.max(0, Math.trunc(storedConsumed)),
|
|
404
|
+
active,
|
|
405
|
+
eggUsage: Math.max(0, asInt(parsed.eggUsage, 0)),
|
|
406
|
+
eggTier: eggTier === null || eggTier === "legendary" ? null : eggTier,
|
|
407
|
+
pendingHatch,
|
|
408
|
+
pendingReveal,
|
|
409
|
+
lure: parsed.lure === true,
|
|
410
|
+
incense: parsed.incense === true,
|
|
411
|
+
repel: typeof parsed.repel === "number" && Number.isInteger(parsed.repel) && parsed.repel > 0 ? parsed.repel : null,
|
|
412
|
+
inventory
|
|
413
|
+
};
|
|
249
414
|
}
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
return node;
|
|
253
|
-
for (const child of node.evolvesTo) {
|
|
254
|
-
const found = nodeFor(child, id);
|
|
255
|
-
if (found !== null)
|
|
256
|
-
return found;
|
|
257
|
-
}
|
|
258
|
-
return null;
|
|
415
|
+
function serialiseState(state) {
|
|
416
|
+
return JSON.stringify(state);
|
|
259
417
|
}
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
if (inFlight !== undefined)
|
|
263
|
-
return inFlight;
|
|
264
|
-
const pending = (async () => {
|
|
265
|
-
const path = chainPath(chainId);
|
|
266
|
-
const cached = await readJson(deps, path);
|
|
267
|
-
if (cached !== null) {
|
|
268
|
-
const parsed2 = parseChainNode(cached);
|
|
269
|
-
if (parsed2 !== null)
|
|
270
|
-
return parsed2;
|
|
271
|
-
}
|
|
272
|
-
const fetched = await fetchJson(deps, `${POKEAPI_ORIGIN}/api/v2/evolution-chain/${chainId}`);
|
|
273
|
-
if (fetched === null)
|
|
274
|
-
return null;
|
|
275
|
-
const root = asRecord(fetched)?.chain;
|
|
276
|
-
const parsed = parseChainNode(root);
|
|
277
|
-
if (parsed === null)
|
|
278
|
-
return null;
|
|
279
|
-
await writeJson(deps, path, { chain: root });
|
|
280
|
-
return parsed;
|
|
281
|
-
})();
|
|
282
|
-
cache.set(chainId, pending);
|
|
283
|
-
return pending;
|
|
418
|
+
function hasShinyCharm(state) {
|
|
419
|
+
return (state.inventory.shinyCharm ?? 0) > 0;
|
|
284
420
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
421
|
+
|
|
422
|
+
// src/store.ts
|
|
423
|
+
var MIGRATIONS = [
|
|
424
|
+
{
|
|
425
|
+
version: 1,
|
|
426
|
+
sql: `
|
|
427
|
+
CREATE TABLE {{companion}} (
|
|
428
|
+
api_key_id TEXT PRIMARY KEY,
|
|
429
|
+
state TEXT NOT NULL,
|
|
430
|
+
tokens_total INTEGER NOT NULL DEFAULT 0,
|
|
431
|
+
tokens_spent INTEGER NOT NULL DEFAULT 0,
|
|
432
|
+
created_at INTEGER NOT NULL,
|
|
433
|
+
updated_at INTEGER NOT NULL
|
|
434
|
+
)
|
|
435
|
+
`
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
version: 2,
|
|
439
|
+
sql: `
|
|
440
|
+
CREATE TABLE {{dex}} (
|
|
441
|
+
id TEXT PRIMARY KEY,
|
|
442
|
+
api_key_id TEXT NOT NULL,
|
|
443
|
+
base_id INTEGER NOT NULL,
|
|
444
|
+
final_id INTEGER NOT NULL,
|
|
445
|
+
chain_order TEXT NOT NULL,
|
|
446
|
+
rarity TEXT NOT NULL,
|
|
447
|
+
is_shiny INTEGER NOT NULL DEFAULT 0,
|
|
448
|
+
nature TEXT,
|
|
449
|
+
caught_at INTEGER NOT NULL
|
|
450
|
+
)
|
|
451
|
+
`
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
version: 3,
|
|
455
|
+
sql: `CREATE INDEX {{dex_by_key}} ON {{dex}} (api_key_id, caught_at DESC)`
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
version: 4,
|
|
459
|
+
sql: `
|
|
460
|
+
CREATE TABLE {{grants}} (
|
|
461
|
+
api_key_id TEXT NOT NULL,
|
|
462
|
+
window_key TEXT NOT NULL,
|
|
463
|
+
-- An instant, not a tier. A grant is rate-limited by the window's own
|
|
464
|
+
-- duration, because nothing tells this plugin when a window empties.
|
|
465
|
+
granted_at INTEGER NOT NULL,
|
|
466
|
+
PRIMARY KEY (api_key_id, window_key)
|
|
467
|
+
)
|
|
468
|
+
`
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
version: 5,
|
|
472
|
+
sql: `ALTER TABLE {{companion}} ADD COLUMN last_credit_at INTEGER`
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
version: 6,
|
|
476
|
+
sql: `ALTER TABLE {{dex}} ADD COLUMN stage_times TEXT`
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
version: 7,
|
|
480
|
+
sql: `
|
|
481
|
+
CREATE TABLE {{sightings}} (
|
|
482
|
+
api_key_id TEXT NOT NULL,
|
|
483
|
+
species_id INTEGER NOT NULL,
|
|
484
|
+
-- The line this individual was on, so a species seen but never
|
|
485
|
+
-- graduated still has a chain to draw. There is no Dex row to take one
|
|
486
|
+
-- from, and a record with no line is a sprite with nothing under it.
|
|
487
|
+
chain_order TEXT NOT NULL,
|
|
488
|
+
rarity TEXT NOT NULL,
|
|
489
|
+
is_shiny INTEGER NOT NULL DEFAULT 0,
|
|
490
|
+
seen_at INTEGER NOT NULL,
|
|
491
|
+
PRIMARY KEY (api_key_id, species_id)
|
|
492
|
+
)
|
|
493
|
+
`
|
|
295
494
|
}
|
|
296
|
-
|
|
495
|
+
];
|
|
496
|
+
function wallet(row) {
|
|
497
|
+
return Math.max(0, row.tokensTotal - row.tokensSpent);
|
|
297
498
|
}
|
|
298
|
-
function
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const captureRate = asFiniteNumber(record.captureRate);
|
|
303
|
-
if (captureRate === null)
|
|
304
|
-
return null;
|
|
305
|
-
const chainRaw = asArray(record.chain);
|
|
306
|
-
if (chainRaw === null || chainRaw.length === 0)
|
|
499
|
+
function readCompanion(storage, apiKeyId) {
|
|
500
|
+
const row = storage.get(`SELECT api_key_id, state, tokens_total, tokens_spent, last_credit_at
|
|
501
|
+
FROM {{companion}} WHERE api_key_id = ?`, [apiKeyId]);
|
|
502
|
+
if (row === null)
|
|
307
503
|
return null;
|
|
308
|
-
const chain = [];
|
|
309
|
-
for (const entry of chainRaw) {
|
|
310
|
-
if (typeof entry !== "number" || !isFetchableSpeciesId(entry))
|
|
311
|
-
return null;
|
|
312
|
-
chain.push(entry);
|
|
313
|
-
}
|
|
314
504
|
return {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
chain
|
|
505
|
+
apiKeyId: row.api_key_id,
|
|
506
|
+
state: parseState(row.state),
|
|
507
|
+
tokensTotal: row.tokens_total,
|
|
508
|
+
tokensSpent: row.tokens_spent,
|
|
509
|
+
lastCreditAt: row.last_credit_at
|
|
321
510
|
};
|
|
322
511
|
}
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
};
|
|
512
|
+
function listCompanions(storage) {
|
|
513
|
+
const rows = storage.all(`SELECT api_key_id, state, tokens_total, tokens_spent, last_credit_at
|
|
514
|
+
FROM {{companion}}
|
|
515
|
+
ORDER BY last_credit_at IS NULL, last_credit_at DESC, tokens_total DESC, api_key_id ASC`);
|
|
516
|
+
return rows.map((row) => ({
|
|
517
|
+
apiKeyId: row.api_key_id,
|
|
518
|
+
state: parseState(row.state),
|
|
519
|
+
tokensTotal: row.tokens_total,
|
|
520
|
+
tokensSpent: row.tokens_spent,
|
|
521
|
+
lastCreditAt: row.last_credit_at
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
function creditTokens(storage, apiKeyId, tokens, now) {
|
|
525
|
+
if (tokens <= 0)
|
|
526
|
+
return;
|
|
527
|
+
storage.run(`INSERT INTO {{companion}} (api_key_id, state, tokens_total, tokens_spent, created_at, updated_at, last_credit_at)
|
|
528
|
+
VALUES (?, ?, ?, 0, ?, ?, ?)
|
|
529
|
+
ON CONFLICT(api_key_id) DO UPDATE SET
|
|
530
|
+
tokens_total = tokens_total + excluded.tokens_total,
|
|
531
|
+
updated_at = excluded.updated_at,
|
|
532
|
+
last_credit_at = excluded.last_credit_at`, [apiKeyId, serialiseState(freshState()), Math.trunc(tokens), now, now, now]);
|
|
533
|
+
}
|
|
534
|
+
function settle(storage, apiKeyId, now) {
|
|
535
|
+
const row = readCompanion(storage, apiKeyId);
|
|
536
|
+
if (row === null)
|
|
537
|
+
return null;
|
|
538
|
+
if (row.state === null)
|
|
539
|
+
return { row, events: [] };
|
|
540
|
+
const result = advance(row.state, row.tokensTotal, now);
|
|
541
|
+
if (result.events.length === 0 && result.state === row.state)
|
|
542
|
+
return { row, events: [] };
|
|
543
|
+
storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
|
|
544
|
+
serialiseState(result.state),
|
|
545
|
+
now,
|
|
546
|
+
apiKeyId
|
|
547
|
+
]);
|
|
548
|
+
return { row: { ...row, state: result.state }, events: result.events };
|
|
549
|
+
}
|
|
550
|
+
function recordGraduation(storage, apiKeyId, entry, id) {
|
|
551
|
+
storage.run(`INSERT INTO {{dex}} (id, api_key_id, base_id, final_id, chain_order, stage_times, rarity, is_shiny, nature, caught_at)
|
|
552
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
553
|
+
id,
|
|
554
|
+
apiKeyId,
|
|
555
|
+
entry.baseId,
|
|
556
|
+
entry.finalId,
|
|
557
|
+
JSON.stringify(entry.chainOrder),
|
|
558
|
+
entry.stageTimes === null ? null : JSON.stringify(entry.stageTimes),
|
|
559
|
+
entry.rarity,
|
|
560
|
+
entry.isShiny ? 1 : 0,
|
|
561
|
+
entry.nature,
|
|
562
|
+
entry.caughtAt
|
|
563
|
+
]);
|
|
334
564
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (cached !== null)
|
|
341
|
-
return cached;
|
|
342
|
-
const raw = asRecord(await fetchJson(deps, `${POKEAPI_ORIGIN}/api/v2/pokemon-species/${id}`));
|
|
343
|
-
if (raw === null)
|
|
344
|
-
return null;
|
|
345
|
-
const captureRate = asFiniteNumber(raw.capture_rate);
|
|
346
|
-
if (captureRate === null)
|
|
565
|
+
function parseChain(raw) {
|
|
566
|
+
let parsed;
|
|
567
|
+
try {
|
|
568
|
+
parsed = JSON.parse(raw);
|
|
569
|
+
} catch {
|
|
347
570
|
return null;
|
|
348
|
-
|
|
349
|
-
if (
|
|
571
|
+
}
|
|
572
|
+
if (!Array.isArray(parsed))
|
|
350
573
|
return null;
|
|
351
|
-
const
|
|
352
|
-
|
|
574
|
+
const chain = parsed.filter((id) => typeof id === "number");
|
|
575
|
+
return chain.length === 0 ? null : chain;
|
|
576
|
+
}
|
|
577
|
+
function parseStageTimes(raw) {
|
|
578
|
+
if (raw === null)
|
|
353
579
|
return null;
|
|
354
|
-
|
|
355
|
-
|
|
580
|
+
let parsed;
|
|
581
|
+
try {
|
|
582
|
+
parsed = JSON.parse(raw);
|
|
583
|
+
} catch {
|
|
356
584
|
return null;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
names: parseNames(raw.names),
|
|
360
|
-
captureRate,
|
|
361
|
-
isLegendary: raw.is_legendary === true,
|
|
362
|
-
isMythical: raw.is_mythical === true,
|
|
363
|
-
chain
|
|
364
|
-
};
|
|
365
|
-
await writeJson(deps, path, detailToCache(detail));
|
|
366
|
-
return detail;
|
|
367
|
-
}
|
|
368
|
-
function speciesDetail(deps, id) {
|
|
369
|
-
return loadDetail(deps, id, new Map);
|
|
370
|
-
}
|
|
371
|
-
function speciesDetails(deps, ids) {
|
|
372
|
-
const chains = new Map;
|
|
373
|
-
return Promise.all(ids.map((id) => loadDetail(deps, id, chains)));
|
|
374
|
-
}
|
|
375
|
-
async function cachedSpeciesName(deps, id) {
|
|
376
|
-
if (!isFetchableSpeciesId(id))
|
|
585
|
+
}
|
|
586
|
+
if (!Array.isArray(parsed))
|
|
377
587
|
return null;
|
|
378
|
-
|
|
379
|
-
return cached?.names.en ?? null;
|
|
588
|
+
return parsed.map((at) => typeof at === "number" && Number.isFinite(at) ? at : null);
|
|
380
589
|
}
|
|
381
|
-
function
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
590
|
+
function readDex(storage, apiKeyId) {
|
|
591
|
+
const rows = storage.all(`SELECT id, base_id, final_id, chain_order, stage_times, rarity, is_shiny, nature, caught_at
|
|
592
|
+
FROM {{dex}} WHERE api_key_id = ? ORDER BY caught_at DESC`, [apiKeyId]);
|
|
593
|
+
const entries = [];
|
|
594
|
+
for (const row of rows) {
|
|
595
|
+
const chain = parseChain(row.chain_order);
|
|
596
|
+
if (chain === null)
|
|
597
|
+
continue;
|
|
598
|
+
entries.push({
|
|
599
|
+
id: row.id,
|
|
600
|
+
baseId: row.base_id,
|
|
601
|
+
finalId: row.final_id,
|
|
602
|
+
chainOrder: chain,
|
|
603
|
+
stageTimes: parseStageTimes(row.stage_times),
|
|
604
|
+
rarity: row.rarity,
|
|
605
|
+
isShiny: row.is_shiny === 1,
|
|
606
|
+
nature: row.nature,
|
|
607
|
+
caughtAt: row.caught_at
|
|
608
|
+
});
|
|
399
609
|
}
|
|
400
|
-
return
|
|
610
|
+
return entries;
|
|
401
611
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
612
|
+
function recordSightings(storage, apiKeyId, reached, now) {
|
|
613
|
+
if (reached.disguised)
|
|
614
|
+
return;
|
|
615
|
+
const chain = JSON.stringify(reached.plannedPath);
|
|
616
|
+
const stages = reached.plannedPath.slice(0, reached.stageIndex + 1);
|
|
617
|
+
for (const [stage, speciesId] of stages.entries()) {
|
|
618
|
+
storage.run(`INSERT INTO {{sightings}} (api_key_id, species_id, chain_order, rarity, is_shiny, seen_at)
|
|
619
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
620
|
+
ON CONFLICT(api_key_id, species_id) DO NOTHING`, [
|
|
621
|
+
apiKeyId,
|
|
622
|
+
speciesId,
|
|
623
|
+
chain,
|
|
624
|
+
reached.rarity,
|
|
625
|
+
reached.isShiny ? 1 : 0,
|
|
626
|
+
reached.stageTimes[stage] ?? now
|
|
627
|
+
]);
|
|
628
|
+
}
|
|
413
629
|
}
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const candidates = [];
|
|
422
|
-
for (const detail of details) {
|
|
423
|
-
if (detail === null)
|
|
424
|
-
continue;
|
|
425
|
-
if (detail.chain[0] !== detail.id)
|
|
630
|
+
function listSightings(storage, apiKeyId) {
|
|
631
|
+
const rows = storage.all(`SELECT species_id, chain_order, rarity, is_shiny, seen_at
|
|
632
|
+
FROM {{sightings}} WHERE api_key_id = ? ORDER BY species_id ASC`, [apiKeyId]);
|
|
633
|
+
const sightings = [];
|
|
634
|
+
for (const row of rows) {
|
|
635
|
+
const chain = parseChain(row.chain_order);
|
|
636
|
+
if (chain === null)
|
|
426
637
|
continue;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
638
|
+
sightings.push({
|
|
639
|
+
speciesId: row.species_id,
|
|
640
|
+
chainOrder: chain,
|
|
641
|
+
rarity: row.rarity,
|
|
642
|
+
isShiny: row.is_shiny === 1,
|
|
643
|
+
seenAt: row.seen_at
|
|
432
644
|
});
|
|
433
645
|
}
|
|
434
|
-
|
|
435
|
-
await writeJson(deps, INDEX_PATH, candidates);
|
|
436
|
-
return candidates;
|
|
646
|
+
return sightings;
|
|
437
647
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const path = spritePath(id, shiny);
|
|
442
|
-
try {
|
|
443
|
-
const cached = await deps.files.read(path);
|
|
444
|
-
if (cached !== null && cached.length > 0)
|
|
445
|
-
return cached;
|
|
446
|
-
} catch {}
|
|
447
|
-
const url = shiny ? `${SPRITE_ORIGIN}${SPRITE_DIR}/shiny/${id}.gif` : `${SPRITE_ORIGIN}${SPRITE_DIR}/${id}.gif`;
|
|
448
|
-
const bytes = await fetchBytes(deps, url);
|
|
449
|
-
if (bytes === null)
|
|
450
|
-
return null;
|
|
451
|
-
await writeCache(deps, path, bytes);
|
|
452
|
-
return bytes;
|
|
648
|
+
function lastGrantedAt(storage, apiKeyId, windowKey) {
|
|
649
|
+
const row = storage.get("SELECT granted_at FROM {{grants}} WHERE api_key_id = ? AND window_key = ?", [apiKeyId, windowKey]);
|
|
650
|
+
return row?.granted_at ?? null;
|
|
453
651
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return null;
|
|
458
|
-
const path = itemSpritePath(name);
|
|
459
|
-
try {
|
|
460
|
-
const cached = await deps.files.read(path);
|
|
461
|
-
if (cached !== null && cached.length > 0)
|
|
462
|
-
return cached;
|
|
463
|
-
} catch {}
|
|
464
|
-
const bytes = await fetchBytes(deps, `${SPRITE_ORIGIN}${ITEM_SPRITE_DIR}/${name}.png`);
|
|
465
|
-
if (bytes === null)
|
|
466
|
-
return null;
|
|
467
|
-
await writeCache(deps, path, bytes);
|
|
468
|
-
return bytes;
|
|
652
|
+
function setGrantedAt(storage, apiKeyId, windowKey, at) {
|
|
653
|
+
storage.run(`INSERT INTO {{grants}} (api_key_id, window_key, granted_at) VALUES (?, ?, ?)
|
|
654
|
+
ON CONFLICT(api_key_id, window_key) DO UPDATE SET granted_at = excluded.granted_at`, [apiKeyId, windowKey, at]);
|
|
469
655
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
var NATURES = [
|
|
473
|
-
"hardy",
|
|
474
|
-
"lonely",
|
|
475
|
-
"brave",
|
|
476
|
-
"adamant",
|
|
477
|
-
"naughty",
|
|
478
|
-
"bold",
|
|
479
|
-
"docile",
|
|
480
|
-
"relaxed",
|
|
481
|
-
"impish",
|
|
482
|
-
"lax",
|
|
483
|
-
"timid",
|
|
484
|
-
"hasty",
|
|
485
|
-
"serious",
|
|
486
|
-
"jolly",
|
|
487
|
-
"naive",
|
|
488
|
-
"modest",
|
|
489
|
-
"mild",
|
|
490
|
-
"quiet",
|
|
491
|
-
"bashful",
|
|
492
|
-
"rash",
|
|
493
|
-
"calm",
|
|
494
|
-
"gentle",
|
|
495
|
-
"sassy",
|
|
496
|
-
"careful",
|
|
497
|
-
"quirky"
|
|
498
|
-
];
|
|
499
|
-
function mulberry32(seed) {
|
|
500
|
-
let state = seed >>> 0;
|
|
501
|
-
return () => {
|
|
502
|
-
state = state + 1831565813 >>> 0;
|
|
503
|
-
let t = state;
|
|
504
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
505
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
506
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
507
|
-
};
|
|
656
|
+
function shopPrice(entry) {
|
|
657
|
+
return entry.kind === "item" ? ITEM_PRICES[entry.item] : freshEggPrice(entry.tier);
|
|
508
658
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
return sortRank(rarity2) >= sortRank(input.guarantee);
|
|
526
|
-
});
|
|
527
|
-
const wanted = input.onlyUncollected === true;
|
|
528
|
-
const lured = wanted ? eligibleWith(true) : [];
|
|
529
|
-
const usedLure = wanted && lured.length > 0;
|
|
530
|
-
const eligible = usedLure ? lured : eligibleWith(false);
|
|
531
|
-
if (eligible.length === 0)
|
|
532
|
-
return null;
|
|
533
|
-
const weights = eligible.map((candidate) => {
|
|
534
|
-
const base = Math.max(1, candidate.captureRate);
|
|
535
|
-
const collected = input.collectedFinals.has(candidate.finalId) ? base * COLLECTED_WEIGHT : base;
|
|
536
|
-
return input.preferLongLines === true ? collected * (1 + FORM_WEIGHT * (Math.max(1, candidate.forms) - 1)) : collected;
|
|
537
|
-
});
|
|
538
|
-
const total = weights.reduce((a, b) => a + b, 0);
|
|
539
|
-
let target = random() * total;
|
|
540
|
-
let index = 0;
|
|
541
|
-
for (let i = 0;i < weights.length; i++) {
|
|
542
|
-
target -= weights[i];
|
|
543
|
-
if (target <= 0) {
|
|
544
|
-
index = i;
|
|
545
|
-
break;
|
|
659
|
+
function consume(storage, apiKeyId, item, applyToState, now) {
|
|
660
|
+
const row = readCompanion(storage, apiKeyId);
|
|
661
|
+
if (row === null)
|
|
662
|
+
return { ok: false, reason: "missing" };
|
|
663
|
+
if (row.state === null)
|
|
664
|
+
return { ok: false, reason: "unreadable" };
|
|
665
|
+
if ((row.state.inventory[item] ?? 0) <= 0)
|
|
666
|
+
return { ok: false, reason: "none-held" };
|
|
667
|
+
const outcome = applyToState(row.state);
|
|
668
|
+
if ("refused" in outcome)
|
|
669
|
+
return { ok: false, reason: outcome.refused };
|
|
670
|
+
const nextState = {
|
|
671
|
+
...outcome.applied,
|
|
672
|
+
inventory: {
|
|
673
|
+
...outcome.applied.inventory,
|
|
674
|
+
[item]: (outcome.applied.inventory[item] ?? 0) - 1
|
|
546
675
|
}
|
|
676
|
+
};
|
|
677
|
+
storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
|
|
678
|
+
serialiseState(nextState),
|
|
679
|
+
now,
|
|
680
|
+
apiKeyId
|
|
681
|
+
]);
|
|
682
|
+
return { ok: true, row: { ...row, state: nextState } };
|
|
683
|
+
}
|
|
684
|
+
function purchase(storage, apiKeyId, entry, applyToState, now) {
|
|
685
|
+
const price = shopPrice(entry);
|
|
686
|
+
{
|
|
687
|
+
const row = readCompanion(storage, apiKeyId);
|
|
688
|
+
if (row === null)
|
|
689
|
+
return { ok: false, reason: "missing" };
|
|
690
|
+
if (row.state === null)
|
|
691
|
+
return { ok: false, reason: "unreadable" };
|
|
692
|
+
if (wallet(row) < price)
|
|
693
|
+
return { ok: false, reason: "insufficient" };
|
|
694
|
+
const outcome = applyToState(row.state);
|
|
695
|
+
if ("refused" in outcome)
|
|
696
|
+
return { ok: false, reason: outcome.refused };
|
|
697
|
+
const nextState = outcome.applied;
|
|
698
|
+
storage.run("UPDATE {{companion}} SET state = ?, tokens_spent = tokens_spent + ?, updated_at = ? WHERE api_key_id = ?", [serialiseState(nextState), price, now, apiKeyId]);
|
|
699
|
+
return {
|
|
700
|
+
ok: true,
|
|
701
|
+
row: { ...row, state: nextState, tokensSpent: row.tokensSpent + price }
|
|
702
|
+
};
|
|
547
703
|
}
|
|
548
|
-
const chosen = eligible[index];
|
|
549
|
-
const shinyDenominator = input.hasShinyCharm ? ODDS.shinyWithCharm : ODDS.shiny;
|
|
550
|
-
const isShiny = random() < 1 / shinyDenominator;
|
|
551
|
-
const nature = NATURES[Math.floor(random() * NATURES.length)];
|
|
552
|
-
const rarity = rarityFromCaptureRate(chosen.captureRate, false, false);
|
|
553
|
-
const dittoEligible = rarity === "common" && chosen.forms >= 2;
|
|
554
|
-
const ditto = dittoEligible && random() < 1 / ODDS.dittoDisguise;
|
|
555
|
-
return { speciesId: chosen.id, isShiny, nature, ditto, usedLure };
|
|
556
704
|
}
|
|
557
705
|
|
|
558
|
-
// src/
|
|
559
|
-
function
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
706
|
+
// src/collection.ts
|
|
707
|
+
function collect(entries, sightings = []) {
|
|
708
|
+
const bySpecies = new Map;
|
|
709
|
+
const contributions = [...entries.flatMap(contributionsOf), ...sightings.map(contributionOf)];
|
|
710
|
+
for (const { speciesId, stamp, exact, source, taken } of contributions) {
|
|
711
|
+
const found = bySpecies.get(speciesId);
|
|
712
|
+
if (found === undefined) {
|
|
713
|
+
bySpecies.set(speciesId, {
|
|
714
|
+
rarity: source.rarity,
|
|
715
|
+
isShiny: source.isShiny,
|
|
716
|
+
first: stamp,
|
|
717
|
+
firstExact: exact,
|
|
718
|
+
catches: taken === null ? [] : [taken],
|
|
719
|
+
lines: new Map([[lineKey(source.chainOrder), source.chainOrder]])
|
|
720
|
+
});
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (taken !== null)
|
|
724
|
+
found.catches.push(taken);
|
|
725
|
+
found.isShiny = found.isShiny || source.isShiny;
|
|
726
|
+
if (earlier(stamp, found.first)) {
|
|
727
|
+
found.rarity = source.rarity;
|
|
728
|
+
found.first = stamp;
|
|
729
|
+
found.firstExact = exact;
|
|
730
|
+
}
|
|
731
|
+
const key = lineKey(source.chainOrder);
|
|
732
|
+
if (!found.lines.has(key))
|
|
733
|
+
found.lines.set(key, source.chainOrder);
|
|
734
|
+
}
|
|
735
|
+
return [...bySpecies].map(([speciesId, found]) => ({
|
|
736
|
+
speciesId,
|
|
737
|
+
rarity: found.rarity,
|
|
738
|
+
isShiny: found.isShiny,
|
|
739
|
+
firstCaughtAt: found.first.caughtAt,
|
|
740
|
+
firstCaughtExact: found.firstExact,
|
|
741
|
+
lines: [...found.lines.values()],
|
|
742
|
+
catches: [...found.catches].sort((a, b) => (b.enteredAt ?? b.caughtAt) - (a.enteredAt ?? a.caughtAt) || byId(a.id, b.id))
|
|
743
|
+
})).sort((a, b) => a.speciesId - b.speciesId);
|
|
744
|
+
}
|
|
745
|
+
function readCollection(storage, apiKeyId) {
|
|
746
|
+
return collect(readDex(storage, apiKeyId), listSightings(storage, apiKeyId));
|
|
747
|
+
}
|
|
748
|
+
function contributionsOf(entry) {
|
|
749
|
+
return entry.chainOrder.map((speciesId) => {
|
|
750
|
+
const enteredAt = enteredAtOf(entry, speciesId);
|
|
751
|
+
return {
|
|
752
|
+
speciesId,
|
|
753
|
+
stamp: { id: entry.id, caughtAt: enteredAt ?? entry.caughtAt },
|
|
754
|
+
exact: enteredAt !== null,
|
|
755
|
+
source: entry,
|
|
756
|
+
taken: {
|
|
757
|
+
id: entry.id,
|
|
758
|
+
chainOrder: entry.chainOrder,
|
|
759
|
+
isShiny: entry.isShiny,
|
|
760
|
+
nature: entry.nature,
|
|
761
|
+
caughtAt: entry.caughtAt,
|
|
762
|
+
enteredAt
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
});
|
|
564
766
|
}
|
|
565
|
-
function
|
|
767
|
+
function contributionOf(sighting) {
|
|
566
768
|
return {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
pendingReveal: null,
|
|
573
|
-
lure: false,
|
|
574
|
-
incense: false,
|
|
575
|
-
repel: null,
|
|
576
|
-
inventory: emptyInventory()
|
|
769
|
+
speciesId: sighting.speciesId,
|
|
770
|
+
stamp: { id: `seen-${sighting.speciesId}`, caughtAt: sighting.seenAt },
|
|
771
|
+
exact: true,
|
|
772
|
+
source: sighting,
|
|
773
|
+
taken: null
|
|
577
774
|
};
|
|
578
775
|
}
|
|
579
|
-
function
|
|
580
|
-
return
|
|
776
|
+
function lineKey(chainOrder) {
|
|
777
|
+
return chainOrder.join("-");
|
|
581
778
|
}
|
|
582
|
-
function
|
|
583
|
-
|
|
779
|
+
function enteredAtOf(entry, speciesId) {
|
|
780
|
+
if (entry.stageTimes === null)
|
|
781
|
+
return null;
|
|
782
|
+
const stage = entry.chainOrder.indexOf(speciesId);
|
|
783
|
+
if (stage < 0)
|
|
784
|
+
return null;
|
|
785
|
+
return entry.stageTimes[stage] ?? null;
|
|
584
786
|
}
|
|
585
|
-
function
|
|
586
|
-
|
|
787
|
+
function earlier(a, b) {
|
|
788
|
+
if (a.caughtAt !== b.caughtAt)
|
|
789
|
+
return a.caughtAt < b.caughtAt;
|
|
790
|
+
return byId(a.id, b.id) < 0;
|
|
587
791
|
}
|
|
588
|
-
function
|
|
589
|
-
|
|
792
|
+
function byId(a, b) {
|
|
793
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// node_modules/@omnigateway/plugin-api/src/events.ts
|
|
797
|
+
var WINDOW_MS = {
|
|
798
|
+
"1m": 60000,
|
|
799
|
+
"5h": 5 * 60 * 60 * 1000,
|
|
800
|
+
"1w": 7 * 24 * 60 * 60 * 1000
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// src/grants.ts
|
|
804
|
+
function windowKey(event) {
|
|
805
|
+
return `${event.dimension}:${event.window}`;
|
|
806
|
+
}
|
|
807
|
+
function grantSize(window) {
|
|
808
|
+
if (window === "1w")
|
|
809
|
+
return 5;
|
|
810
|
+
if (window === "5h")
|
|
811
|
+
return 1;
|
|
812
|
+
return 0;
|
|
813
|
+
}
|
|
814
|
+
function decideGrant(input) {
|
|
815
|
+
if (grantSize(input.window) === 0)
|
|
816
|
+
return { grant: false };
|
|
817
|
+
if (input.lastGrantedAt === null)
|
|
818
|
+
return { grant: false, seedAt: input.now };
|
|
819
|
+
if (input.now - input.lastGrantedAt < WINDOW_MS[input.window])
|
|
820
|
+
return { grant: false };
|
|
821
|
+
return { grant: true, count: grantSize(input.window), at: input.now };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/pokeapi.ts
|
|
825
|
+
var POKEAPI_ORIGIN = "https://pokeapi.co";
|
|
826
|
+
var SPRITE_ORIGIN = "https://raw.githubusercontent.com";
|
|
827
|
+
var SPRITE_DIR = "/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated";
|
|
828
|
+
var ITEM_SPRITE_DIR = "/PokeAPI/sprites/master/sprites/items";
|
|
829
|
+
var MAX_EVOLUTION_CHAIN_ID = 2000;
|
|
830
|
+
var INDEX_FETCH_CONCURRENCY = 8;
|
|
831
|
+
var INDEX_PATH = "species/index.json";
|
|
832
|
+
var speciesPath = (id) => `species/${id}.json`;
|
|
833
|
+
var chainPath = (chainId) => `species/chain-${chainId}.json`;
|
|
834
|
+
var spritePath = (id, shiny) => shiny ? `sprites/shiny/${id}.gif` : `sprites/${id}.gif`;
|
|
835
|
+
var itemSpritePath = (name) => `sprites/items/${name}.png`;
|
|
836
|
+
function isFetchableSpeciesId(id) {
|
|
837
|
+
return Number.isInteger(id) && hasAnimatedSprite(id);
|
|
838
|
+
}
|
|
839
|
+
function asRecord(value) {
|
|
840
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
841
|
+
return null;
|
|
842
|
+
return value;
|
|
843
|
+
}
|
|
844
|
+
function asArray(value) {
|
|
845
|
+
return Array.isArray(value) ? value : null;
|
|
846
|
+
}
|
|
847
|
+
function asFiniteNumber(value) {
|
|
848
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
849
|
+
}
|
|
850
|
+
var encoder = new TextEncoder;
|
|
851
|
+
var decoder = new TextDecoder;
|
|
852
|
+
async function readJson(deps, path) {
|
|
590
853
|
try {
|
|
591
|
-
|
|
854
|
+
const bytes = await deps.files.read(path);
|
|
855
|
+
if (bytes === null)
|
|
856
|
+
return null;
|
|
857
|
+
return JSON.parse(decoder.decode(bytes));
|
|
592
858
|
} catch {
|
|
593
859
|
return null;
|
|
594
860
|
}
|
|
595
|
-
|
|
861
|
+
}
|
|
862
|
+
async function writeCache(deps, path, bytes) {
|
|
863
|
+
try {
|
|
864
|
+
await deps.files.write(path, bytes);
|
|
865
|
+
} catch {}
|
|
866
|
+
}
|
|
867
|
+
async function writeJson(deps, path, value) {
|
|
868
|
+
await writeCache(deps, path, encoder.encode(JSON.stringify(value)));
|
|
869
|
+
}
|
|
870
|
+
async function fetchJson(deps, url) {
|
|
871
|
+
try {
|
|
872
|
+
const response = await deps.net(url);
|
|
873
|
+
if (!response.ok)
|
|
874
|
+
return null;
|
|
875
|
+
return JSON.parse(await response.text());
|
|
876
|
+
} catch {
|
|
596
877
|
return null;
|
|
597
|
-
const inventory = emptyInventory();
|
|
598
|
-
const storedInventory = parsed.inventory;
|
|
599
|
-
if (isRecord(storedInventory)) {
|
|
600
|
-
for (const kind of ITEM_KINDS) {
|
|
601
|
-
inventory[kind] = Math.max(0, asInt(storedInventory[kind], 0));
|
|
602
|
-
}
|
|
603
878
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const rarity = asRarity(storedActive.rarity);
|
|
610
|
-
if (rarity === null)
|
|
611
|
-
return null;
|
|
612
|
-
const path = Array.isArray(storedActive.plannedPath) ? storedActive.plannedPath.filter((id) => typeof id === "number" && id > 0) : [];
|
|
613
|
-
if (path.length === 0)
|
|
879
|
+
}
|
|
880
|
+
async function fetchBytes(deps, url) {
|
|
881
|
+
try {
|
|
882
|
+
const response = await deps.net(url);
|
|
883
|
+
if (!response.ok)
|
|
614
884
|
return null;
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
stageIndex: Math.min(Math.max(0, asInt(storedActive.stageIndex, 0)), path.length - 1),
|
|
620
|
-
usedAtStage: Math.max(0, asInt(storedActive.usedAtStage, 0)),
|
|
621
|
-
rarity,
|
|
622
|
-
isShiny: storedActive.isShiny === true,
|
|
623
|
-
nature: nature ?? "hardy",
|
|
624
|
-
dittoDisguise: typeof storedActive.dittoDisguise === "number" ? storedActive.dittoDisguise : null,
|
|
625
|
-
dittoRevealed: storedActive.dittoRevealed === true,
|
|
626
|
-
everstone: storedActive.everstone === true,
|
|
627
|
-
soothe: storedActive.soothe === true,
|
|
628
|
-
soothedRaw: Math.max(0, asInt(storedActive.soothedRaw, 0))
|
|
629
|
-
};
|
|
885
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
886
|
+
return bytes.length === 0 ? null : bytes;
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
630
889
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
890
|
+
}
|
|
891
|
+
var CHAIN_URL_ID = /\/evolution-chain\/(\d+)\/?$/;
|
|
892
|
+
function chainIdFromUrl(url) {
|
|
893
|
+
if (typeof url !== "string")
|
|
894
|
+
return null;
|
|
895
|
+
const match = CHAIN_URL_ID.exec(url);
|
|
896
|
+
if (match === null)
|
|
897
|
+
return null;
|
|
898
|
+
const id = Number(match[1]);
|
|
899
|
+
if (!Number.isInteger(id) || id < 1 || id > MAX_EVOLUTION_CHAIN_ID)
|
|
900
|
+
return null;
|
|
901
|
+
return id;
|
|
902
|
+
}
|
|
903
|
+
var SPECIES_URL_ID = /\/pokemon-species\/(\d+)\/?$/;
|
|
904
|
+
function speciesIdFromUrl(url) {
|
|
905
|
+
if (typeof url !== "string")
|
|
906
|
+
return null;
|
|
907
|
+
const match = SPECIES_URL_ID.exec(url);
|
|
908
|
+
if (match === null)
|
|
909
|
+
return null;
|
|
910
|
+
const id = Number(match[1]);
|
|
911
|
+
return isFetchableSpeciesId(id) ? id : null;
|
|
912
|
+
}
|
|
913
|
+
function parseChainNode(raw) {
|
|
914
|
+
const node = asRecord(raw);
|
|
915
|
+
if (node === null)
|
|
916
|
+
return null;
|
|
917
|
+
const species = asRecord(node.species);
|
|
918
|
+
const id = speciesIdFromUrl(species?.url);
|
|
919
|
+
if (id === null)
|
|
920
|
+
return null;
|
|
921
|
+
const children = asArray(node.evolves_to) ?? [];
|
|
922
|
+
const evolvesTo = [];
|
|
923
|
+
for (const child of children) {
|
|
924
|
+
const parsed = parseChainNode(child);
|
|
925
|
+
if (parsed !== null)
|
|
926
|
+
evolvesTo.push(parsed);
|
|
647
927
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
928
|
+
return { id, evolvesTo };
|
|
929
|
+
}
|
|
930
|
+
function pathTo(node, id) {
|
|
931
|
+
if (node.id === id)
|
|
932
|
+
return [id];
|
|
933
|
+
for (const child of node.evolvesTo) {
|
|
934
|
+
const tail = pathTo(child, id);
|
|
935
|
+
if (tail !== null)
|
|
936
|
+
return [node.id, ...tail];
|
|
655
937
|
}
|
|
656
|
-
|
|
657
|
-
if (typeof storedConsumed !== "number" || !Number.isFinite(storedConsumed))
|
|
658
|
-
return null;
|
|
659
|
-
const eggTier = asRarity(parsed.eggTier);
|
|
660
|
-
return {
|
|
661
|
-
consumedTotal: Math.max(0, Math.trunc(storedConsumed)),
|
|
662
|
-
active,
|
|
663
|
-
eggUsage: Math.max(0, asInt(parsed.eggUsage, 0)),
|
|
664
|
-
eggTier: eggTier === null || eggTier === "legendary" ? null : eggTier,
|
|
665
|
-
pendingHatch,
|
|
666
|
-
pendingReveal,
|
|
667
|
-
lure: parsed.lure === true,
|
|
668
|
-
incense: parsed.incense === true,
|
|
669
|
-
repel: typeof parsed.repel === "number" && Number.isInteger(parsed.repel) && parsed.repel > 0 ? parsed.repel : null,
|
|
670
|
-
inventory
|
|
671
|
-
};
|
|
938
|
+
return null;
|
|
672
939
|
}
|
|
673
|
-
function
|
|
674
|
-
|
|
940
|
+
function descendFirstBranch(node) {
|
|
941
|
+
const rest = [];
|
|
942
|
+
let current = node;
|
|
943
|
+
while (current.evolvesTo.length > 0) {
|
|
944
|
+
const next = current.evolvesTo[0];
|
|
945
|
+
rest.push(next.id);
|
|
946
|
+
current = next;
|
|
947
|
+
}
|
|
948
|
+
return rest;
|
|
675
949
|
}
|
|
676
|
-
function
|
|
677
|
-
|
|
950
|
+
function lineThrough(root, id) {
|
|
951
|
+
const prefix = pathTo(root, id);
|
|
952
|
+
if (prefix === null)
|
|
953
|
+
return null;
|
|
954
|
+
const self = prefix[prefix.length - 1];
|
|
955
|
+
const node = nodeFor(root, self);
|
|
956
|
+
return node === null ? prefix : [...prefix, ...descendFirstBranch(node)];
|
|
678
957
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (gained > 0) {
|
|
687
|
-
const active = next.active;
|
|
688
|
-
if (active === null) {
|
|
689
|
-
next = { ...next, eggUsage: next.eggUsage + gained };
|
|
690
|
-
} else if (!active.soothe) {
|
|
691
|
-
next = { ...next, active: { ...active, usedAtStage: active.usedAtStage + gained } };
|
|
692
|
-
} else {
|
|
693
|
-
const raw = active.soothedRaw + gained;
|
|
694
|
-
const owed = Math.floor(raw * SOOTHE_BONUS) - Math.floor(active.soothedRaw * SOOTHE_BONUS);
|
|
695
|
-
next = {
|
|
696
|
-
...next,
|
|
697
|
-
active: { ...active, soothedRaw: raw, usedAtStage: active.usedAtStage + gained + owed }
|
|
698
|
-
};
|
|
699
|
-
}
|
|
958
|
+
function nodeFor(node, id) {
|
|
959
|
+
if (node.id === id)
|
|
960
|
+
return node;
|
|
961
|
+
for (const child of node.evolvesTo) {
|
|
962
|
+
const found = nodeFor(child, id);
|
|
963
|
+
if (found !== null)
|
|
964
|
+
return found;
|
|
700
965
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
isShiny: hatch.isShiny,
|
|
715
|
-
nature: hatch.nature,
|
|
716
|
-
dittoDisguise: hatch.ditto ? hatch.speciesId : null,
|
|
717
|
-
dittoRevealed: false,
|
|
718
|
-
everstone: false,
|
|
719
|
-
soothe: false,
|
|
720
|
-
soothedRaw: 0
|
|
721
|
-
};
|
|
722
|
-
events.push({
|
|
723
|
-
kind: "hatched",
|
|
724
|
-
speciesId: hatch.speciesId,
|
|
725
|
-
isShiny: active.isShiny,
|
|
726
|
-
ditto: active.dittoDisguise !== null
|
|
727
|
-
});
|
|
728
|
-
next = { ...next, active, eggUsage: 0, eggTier: null, pendingHatch: null };
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
const mon = next.active;
|
|
732
|
-
if (mon.everstone)
|
|
733
|
-
break;
|
|
734
|
-
const needed = phaseThreshold(mon.rarity, mon.plannedPath.length, mon.stageIndex);
|
|
735
|
-
if (mon.usedAtStage < needed)
|
|
736
|
-
break;
|
|
737
|
-
const excess = mon.usedAtStage - needed;
|
|
738
|
-
if (mon.dittoDisguise !== null && !mon.dittoRevealed) {
|
|
739
|
-
if (next.pendingReveal === null)
|
|
740
|
-
break;
|
|
741
|
-
const reveal = next.pendingReveal;
|
|
742
|
-
events.push({
|
|
743
|
-
kind: "revealed",
|
|
744
|
-
disguisedAs: mon.plannedPath[mon.stageIndex] ?? mon.baseId,
|
|
745
|
-
speciesId: reveal.path[0]
|
|
746
|
-
});
|
|
747
|
-
next = {
|
|
748
|
-
...next,
|
|
749
|
-
active: {
|
|
750
|
-
...mon,
|
|
751
|
-
baseId: reveal.path[0],
|
|
752
|
-
plannedPath: reveal.path,
|
|
753
|
-
stageIndex: 0,
|
|
754
|
-
usedAtStage: excess,
|
|
755
|
-
rarity: reveal.rarity,
|
|
756
|
-
dittoRevealed: true
|
|
757
|
-
},
|
|
758
|
-
pendingReveal: null
|
|
759
|
-
};
|
|
760
|
-
continue;
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
function loadChain(deps, chainId, cache) {
|
|
969
|
+
const inFlight = cache.get(chainId);
|
|
970
|
+
if (inFlight !== undefined)
|
|
971
|
+
return inFlight;
|
|
972
|
+
const pending = (async () => {
|
|
973
|
+
const path = chainPath(chainId);
|
|
974
|
+
const cached = await readJson(deps, path);
|
|
975
|
+
if (cached !== null) {
|
|
976
|
+
const parsed2 = parseChainNode(cached);
|
|
977
|
+
if (parsed2 !== null)
|
|
978
|
+
return parsed2;
|
|
761
979
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
980
|
+
const fetched = await fetchJson(deps, `${POKEAPI_ORIGIN}/api/v2/evolution-chain/${chainId}`);
|
|
981
|
+
if (fetched === null)
|
|
982
|
+
return null;
|
|
983
|
+
const root = asRecord(fetched)?.chain;
|
|
984
|
+
const parsed = parseChainNode(root);
|
|
985
|
+
if (parsed === null)
|
|
986
|
+
return null;
|
|
987
|
+
await writeJson(deps, path, { chain: root });
|
|
988
|
+
return parsed;
|
|
989
|
+
})();
|
|
990
|
+
cache.set(chainId, pending);
|
|
991
|
+
return pending;
|
|
992
|
+
}
|
|
993
|
+
function parseNames(raw) {
|
|
994
|
+
const names = {};
|
|
995
|
+
for (const entry of asArray(raw) ?? []) {
|
|
996
|
+
const record = asRecord(entry);
|
|
997
|
+
if (record === null)
|
|
769
998
|
continue;
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
finalId: mon.plannedPath[mon.plannedPath.length - 1],
|
|
775
|
-
chainOrder: mon.plannedPath,
|
|
776
|
-
rarity: mon.rarity,
|
|
777
|
-
isShiny: mon.isShiny,
|
|
778
|
-
nature: mon.nature
|
|
779
|
-
});
|
|
780
|
-
next = { ...next, active: null, eggUsage: excess, eggTier: null, pendingHatch: null };
|
|
999
|
+
const language = asRecord(record.language)?.name;
|
|
1000
|
+
const name = record.name;
|
|
1001
|
+
if (typeof language === "string" && typeof name === "string")
|
|
1002
|
+
names[language] = name;
|
|
781
1003
|
}
|
|
782
|
-
return
|
|
1004
|
+
return names;
|
|
783
1005
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
},
|
|
800
|
-
{
|
|
801
|
-
version: 2,
|
|
802
|
-
sql: `
|
|
803
|
-
CREATE TABLE {{dex}} (
|
|
804
|
-
id TEXT PRIMARY KEY,
|
|
805
|
-
api_key_id TEXT NOT NULL,
|
|
806
|
-
base_id INTEGER NOT NULL,
|
|
807
|
-
final_id INTEGER NOT NULL,
|
|
808
|
-
chain_order TEXT NOT NULL,
|
|
809
|
-
rarity TEXT NOT NULL,
|
|
810
|
-
is_shiny INTEGER NOT NULL DEFAULT 0,
|
|
811
|
-
nature TEXT,
|
|
812
|
-
caught_at INTEGER NOT NULL
|
|
813
|
-
)
|
|
814
|
-
`
|
|
815
|
-
},
|
|
816
|
-
{
|
|
817
|
-
version: 3,
|
|
818
|
-
sql: `CREATE INDEX {{dex_by_key}} ON {{dex}} (api_key_id, caught_at DESC)`
|
|
819
|
-
},
|
|
820
|
-
{
|
|
821
|
-
version: 4,
|
|
822
|
-
sql: `
|
|
823
|
-
CREATE TABLE {{grants}} (
|
|
824
|
-
api_key_id TEXT NOT NULL,
|
|
825
|
-
window_key TEXT NOT NULL,
|
|
826
|
-
-- An instant, not a tier. A grant is rate-limited by the window's own
|
|
827
|
-
-- duration, because nothing tells this plugin when a window empties.
|
|
828
|
-
granted_at INTEGER NOT NULL,
|
|
829
|
-
PRIMARY KEY (api_key_id, window_key)
|
|
830
|
-
)
|
|
831
|
-
`
|
|
832
|
-
},
|
|
833
|
-
{
|
|
834
|
-
version: 5,
|
|
835
|
-
sql: `ALTER TABLE {{companion}} ADD COLUMN last_credit_at INTEGER`
|
|
1006
|
+
function parseCachedDetail(raw, id) {
|
|
1007
|
+
const record = asRecord(raw);
|
|
1008
|
+
if (record === null)
|
|
1009
|
+
return null;
|
|
1010
|
+
const captureRate = asFiniteNumber(record.captureRate);
|
|
1011
|
+
if (captureRate === null)
|
|
1012
|
+
return null;
|
|
1013
|
+
const chainRaw = asArray(record.chain);
|
|
1014
|
+
if (chainRaw === null || chainRaw.length === 0)
|
|
1015
|
+
return null;
|
|
1016
|
+
const chain = [];
|
|
1017
|
+
for (const entry of chainRaw) {
|
|
1018
|
+
if (typeof entry !== "number" || !isFetchableSpeciesId(entry))
|
|
1019
|
+
return null;
|
|
1020
|
+
chain.push(entry);
|
|
836
1021
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1022
|
+
return {
|
|
1023
|
+
id,
|
|
1024
|
+
names: parseNames(record.names),
|
|
1025
|
+
captureRate,
|
|
1026
|
+
isLegendary: record.isLegendary === true,
|
|
1027
|
+
isMythical: record.isMythical === true,
|
|
1028
|
+
chain
|
|
1029
|
+
};
|
|
840
1030
|
}
|
|
841
|
-
function
|
|
842
|
-
const row = storage.get(`SELECT api_key_id, state, tokens_total, tokens_spent, last_credit_at
|
|
843
|
-
FROM {{companion}} WHERE api_key_id = ?`, [apiKeyId]);
|
|
844
|
-
if (row === null)
|
|
845
|
-
return null;
|
|
1031
|
+
function detailToCache(detail) {
|
|
846
1032
|
return {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1033
|
+
captureRate: detail.captureRate,
|
|
1034
|
+
isLegendary: detail.isLegendary,
|
|
1035
|
+
isMythical: detail.isMythical,
|
|
1036
|
+
chain: detail.chain,
|
|
1037
|
+
names: Object.entries(detail.names).map(([language, name]) => ({
|
|
1038
|
+
language: { name: language },
|
|
1039
|
+
name
|
|
1040
|
+
}))
|
|
852
1041
|
};
|
|
853
1042
|
}
|
|
854
|
-
function
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1043
|
+
async function loadDetail(deps, id, chains) {
|
|
1044
|
+
if (!isFetchableSpeciesId(id))
|
|
1045
|
+
return null;
|
|
1046
|
+
const path = speciesPath(id);
|
|
1047
|
+
const cached = parseCachedDetail(await readJson(deps, path), id);
|
|
1048
|
+
if (cached !== null)
|
|
1049
|
+
return cached;
|
|
1050
|
+
const raw = asRecord(await fetchJson(deps, `${POKEAPI_ORIGIN}/api/v2/pokemon-species/${id}`));
|
|
1051
|
+
if (raw === null)
|
|
1052
|
+
return null;
|
|
1053
|
+
const captureRate = asFiniteNumber(raw.capture_rate);
|
|
1054
|
+
if (captureRate === null)
|
|
1055
|
+
return null;
|
|
1056
|
+
const chainId = chainIdFromUrl(asRecord(raw.evolution_chain)?.url);
|
|
1057
|
+
if (chainId === null)
|
|
1058
|
+
return null;
|
|
1059
|
+
const root = await loadChain(deps, chainId, chains);
|
|
1060
|
+
if (root === null)
|
|
1061
|
+
return null;
|
|
1062
|
+
const chain = lineThrough(root, id);
|
|
1063
|
+
if (chain === null || chain.length === 0)
|
|
1064
|
+
return null;
|
|
1065
|
+
const detail = {
|
|
1066
|
+
id,
|
|
1067
|
+
names: parseNames(raw.names),
|
|
1068
|
+
captureRate,
|
|
1069
|
+
isLegendary: raw.is_legendary === true,
|
|
1070
|
+
isMythical: raw.is_mythical === true,
|
|
1071
|
+
chain
|
|
1072
|
+
};
|
|
1073
|
+
await writeJson(deps, path, detailToCache(detail));
|
|
1074
|
+
return detail;
|
|
865
1075
|
}
|
|
866
|
-
function
|
|
867
|
-
|
|
868
|
-
return;
|
|
869
|
-
storage.run(`INSERT INTO {{companion}} (api_key_id, state, tokens_total, tokens_spent, created_at, updated_at, last_credit_at)
|
|
870
|
-
VALUES (?, ?, ?, 0, ?, ?, ?)
|
|
871
|
-
ON CONFLICT(api_key_id) DO UPDATE SET
|
|
872
|
-
tokens_total = tokens_total + excluded.tokens_total,
|
|
873
|
-
updated_at = excluded.updated_at,
|
|
874
|
-
last_credit_at = excluded.last_credit_at`, [apiKeyId, serialiseState(freshState()), Math.trunc(tokens), now, now, now]);
|
|
1076
|
+
function speciesDetail(deps, id) {
|
|
1077
|
+
return loadDetail(deps, id, new Map);
|
|
875
1078
|
}
|
|
876
|
-
function
|
|
877
|
-
const
|
|
878
|
-
|
|
1079
|
+
function speciesDetails(deps, ids) {
|
|
1080
|
+
const chains = new Map;
|
|
1081
|
+
return Promise.all(ids.map((id) => loadDetail(deps, id, chains)));
|
|
1082
|
+
}
|
|
1083
|
+
async function cachedSpeciesName(deps, id) {
|
|
1084
|
+
if (!isFetchableSpeciesId(id))
|
|
879
1085
|
return null;
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const result = advance(row.state, row.tokensTotal);
|
|
883
|
-
if (result.events.length === 0 && result.state === row.state)
|
|
884
|
-
return { row, events: [] };
|
|
885
|
-
storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
|
|
886
|
-
serialiseState(result.state),
|
|
887
|
-
now,
|
|
888
|
-
apiKeyId
|
|
889
|
-
]);
|
|
890
|
-
return { row: { ...row, state: result.state }, events: result.events };
|
|
1086
|
+
const cached = parseCachedDetail(await readJson(deps, speciesPath(id)), id);
|
|
1087
|
+
return cached?.names.en ?? null;
|
|
891
1088
|
}
|
|
892
|
-
function
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
entry
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1089
|
+
function parseCachedIndex(raw) {
|
|
1090
|
+
const entries = asArray(raw);
|
|
1091
|
+
if (entries === null)
|
|
1092
|
+
return null;
|
|
1093
|
+
const candidates = [];
|
|
1094
|
+
for (const entry of entries) {
|
|
1095
|
+
const record = asRecord(entry);
|
|
1096
|
+
if (record === null)
|
|
1097
|
+
return null;
|
|
1098
|
+
const id = asFiniteNumber(record.id);
|
|
1099
|
+
const captureRate = asFiniteNumber(record.captureRate);
|
|
1100
|
+
const forms = asFiniteNumber(record.forms);
|
|
1101
|
+
const finalId = asFiniteNumber(record.finalId);
|
|
1102
|
+
if (id === null || captureRate === null || forms === null || finalId === null)
|
|
1103
|
+
return null;
|
|
1104
|
+
if (!isFetchableSpeciesId(id))
|
|
1105
|
+
return null;
|
|
1106
|
+
candidates.push({ id, captureRate, forms, finalId });
|
|
1107
|
+
}
|
|
1108
|
+
return candidates.length === 0 ? null : candidates;
|
|
905
1109
|
}
|
|
906
|
-
function
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
chainOrder = JSON.parse(row.chain_order);
|
|
914
|
-
} catch {
|
|
915
|
-
continue;
|
|
1110
|
+
async function mapWithConcurrency(ids, limit, worker) {
|
|
1111
|
+
const results = new Array(ids.length);
|
|
1112
|
+
let next = 0;
|
|
1113
|
+
const runners = Array.from({ length: Math.min(limit, ids.length) }, async () => {
|
|
1114
|
+
while (next < ids.length) {
|
|
1115
|
+
const index = next++;
|
|
1116
|
+
results[index] = await worker(ids[index]);
|
|
916
1117
|
}
|
|
917
|
-
|
|
1118
|
+
});
|
|
1119
|
+
await Promise.all(runners);
|
|
1120
|
+
return results;
|
|
1121
|
+
}
|
|
1122
|
+
async function speciesIndex(deps) {
|
|
1123
|
+
const cached = parseCachedIndex(await readJson(deps, INDEX_PATH));
|
|
1124
|
+
if (cached !== null)
|
|
1125
|
+
return cached;
|
|
1126
|
+
const ids = Array.from({ length: ANIMATED_SPECIES_MAX }, (_, i) => i + 1);
|
|
1127
|
+
const chains = new Map;
|
|
1128
|
+
const details = await mapWithConcurrency(ids, INDEX_FETCH_CONCURRENCY, (id) => loadDetail(deps, id, chains));
|
|
1129
|
+
const candidates = [];
|
|
1130
|
+
for (const detail of details) {
|
|
1131
|
+
if (detail === null)
|
|
918
1132
|
continue;
|
|
919
|
-
|
|
920
|
-
if (chain.length === 0)
|
|
1133
|
+
if (detail.chain[0] !== detail.id)
|
|
921
1134
|
continue;
|
|
922
|
-
|
|
923
|
-
id:
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
rarity: row.rarity,
|
|
928
|
-
isShiny: row.is_shiny === 1,
|
|
929
|
-
nature: row.nature,
|
|
930
|
-
caughtAt: row.caught_at
|
|
1135
|
+
candidates.push({
|
|
1136
|
+
id: detail.id,
|
|
1137
|
+
captureRate: detail.captureRate,
|
|
1138
|
+
forms: detail.chain.length,
|
|
1139
|
+
finalId: detail.chain[detail.chain.length - 1] ?? detail.id
|
|
931
1140
|
});
|
|
932
1141
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const row = storage.get("SELECT granted_at FROM {{grants}} WHERE api_key_id = ? AND window_key = ?", [apiKeyId, windowKey2]);
|
|
937
|
-
return row?.granted_at ?? null;
|
|
938
|
-
}
|
|
939
|
-
function setGrantedAt(storage, apiKeyId, windowKey2, at) {
|
|
940
|
-
storage.run(`INSERT INTO {{grants}} (api_key_id, window_key, granted_at) VALUES (?, ?, ?)
|
|
941
|
-
ON CONFLICT(api_key_id, window_key) DO UPDATE SET granted_at = excluded.granted_at`, [apiKeyId, windowKey2, at]);
|
|
942
|
-
}
|
|
943
|
-
function shopPrice(entry) {
|
|
944
|
-
return entry.kind === "item" ? ITEM_PRICES[entry.item] : freshEggPrice(entry.tier);
|
|
1142
|
+
if (details.every((detail) => detail !== null))
|
|
1143
|
+
await writeJson(deps, INDEX_PATH, candidates);
|
|
1144
|
+
return candidates;
|
|
945
1145
|
}
|
|
946
|
-
function
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
[item]: (outcome.applied.inventory[item] ?? 0) - 1
|
|
962
|
-
}
|
|
963
|
-
};
|
|
964
|
-
storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
|
|
965
|
-
serialiseState(nextState),
|
|
966
|
-
now,
|
|
967
|
-
apiKeyId
|
|
968
|
-
]);
|
|
969
|
-
return { ok: true, row: { ...row, state: nextState } };
|
|
1146
|
+
async function spriteBytes(deps, id, shiny) {
|
|
1147
|
+
if (!isFetchableSpeciesId(id))
|
|
1148
|
+
return null;
|
|
1149
|
+
const path = spritePath(id, shiny);
|
|
1150
|
+
try {
|
|
1151
|
+
const cached = await deps.files.read(path);
|
|
1152
|
+
if (cached !== null && cached.length > 0)
|
|
1153
|
+
return cached;
|
|
1154
|
+
} catch {}
|
|
1155
|
+
const url = shiny ? `${SPRITE_ORIGIN}${SPRITE_DIR}/shiny/${id}.gif` : `${SPRITE_ORIGIN}${SPRITE_DIR}/${id}.gif`;
|
|
1156
|
+
const bytes = await fetchBytes(deps, url);
|
|
1157
|
+
if (bytes === null)
|
|
1158
|
+
return null;
|
|
1159
|
+
await writeCache(deps, path, bytes);
|
|
1160
|
+
return bytes;
|
|
970
1161
|
}
|
|
971
|
-
function
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
return {
|
|
987
|
-
ok: true,
|
|
988
|
-
row: { ...row, state: nextState, tokensSpent: row.tokensSpent + price }
|
|
989
|
-
};
|
|
990
|
-
}
|
|
1162
|
+
async function itemSpriteBytes(deps, item) {
|
|
1163
|
+
const name = ITEM_SPRITE_NAMES.get(item);
|
|
1164
|
+
if (name === undefined)
|
|
1165
|
+
return null;
|
|
1166
|
+
const path = itemSpritePath(name);
|
|
1167
|
+
try {
|
|
1168
|
+
const cached = await deps.files.read(path);
|
|
1169
|
+
if (cached !== null && cached.length > 0)
|
|
1170
|
+
return cached;
|
|
1171
|
+
} catch {}
|
|
1172
|
+
const bytes = await fetchBytes(deps, `${SPRITE_ORIGIN}${ITEM_SPRITE_DIR}/${name}.png`);
|
|
1173
|
+
if (bytes === null)
|
|
1174
|
+
return null;
|
|
1175
|
+
await writeCache(deps, path, bytes);
|
|
1176
|
+
return bytes;
|
|
991
1177
|
}
|
|
992
1178
|
|
|
993
1179
|
// src/server.ts
|
|
@@ -1076,6 +1262,17 @@ var server_default = definePlugin({
|
|
|
1076
1262
|
const result = settle(storage, apiKeyId, ctx.now());
|
|
1077
1263
|
if (result === null)
|
|
1078
1264
|
return;
|
|
1265
|
+
const active = result.row.state?.active;
|
|
1266
|
+
if (active !== null && active !== undefined) {
|
|
1267
|
+
recordSightings(storage, apiKeyId, {
|
|
1268
|
+
plannedPath: active.plannedPath,
|
|
1269
|
+
stageIndex: active.stageIndex,
|
|
1270
|
+
stageTimes: active.stageTimes,
|
|
1271
|
+
rarity: active.rarity,
|
|
1272
|
+
isShiny: active.isShiny,
|
|
1273
|
+
disguised: active.dittoDisguise !== null && !active.dittoRevealed
|
|
1274
|
+
}, ctx.now());
|
|
1275
|
+
}
|
|
1079
1276
|
for (const event of result.events) {
|
|
1080
1277
|
if (event.kind !== "graduated")
|
|
1081
1278
|
continue;
|
|
@@ -1083,6 +1280,7 @@ var server_default = definePlugin({
|
|
|
1083
1280
|
baseId: event.baseId,
|
|
1084
1281
|
finalId: event.finalId,
|
|
1085
1282
|
chainOrder: event.chainOrder,
|
|
1283
|
+
stageTimes: event.stageTimes,
|
|
1086
1284
|
rarity: event.rarity,
|
|
1087
1285
|
isShiny: event.isShiny,
|
|
1088
1286
|
nature: event.nature,
|
|
@@ -1252,13 +1450,15 @@ var server_default = definePlugin({
|
|
|
1252
1450
|
if (row.state !== null)
|
|
1253
1451
|
prefetchOnce(apiKeyId, row.state).catch(() => {});
|
|
1254
1452
|
const active = row.state?.active ?? null;
|
|
1255
|
-
const dex = readDex(storage, apiKeyId);
|
|
1256
1453
|
const stageId = active === null ? null : active.plannedPath[active.stageIndex] ?? null;
|
|
1257
1454
|
const stageName = await nameOf(stageId);
|
|
1258
|
-
const named = await Promise.all(
|
|
1455
|
+
const named = await Promise.all(readCollection(storage, apiKeyId).map(async (record) => ({
|
|
1456
|
+
...record,
|
|
1457
|
+
name: await nameOf(record.speciesId)
|
|
1458
|
+
})));
|
|
1259
1459
|
warmNames([
|
|
1260
1460
|
...stageId !== null && stageName === null ? [stageId] : [],
|
|
1261
|
-
...named.filter((
|
|
1461
|
+
...named.filter((record) => record.name === null).map((record) => record.speciesId)
|
|
1262
1462
|
]);
|
|
1263
1463
|
return {
|
|
1264
1464
|
json: {
|