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