@omnigateway/pokemon 1.0.1 → 1.0.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/README.md CHANGED
@@ -56,8 +56,8 @@ check:
56
56
  | Capability | Why |
57
57
  | --- | --- |
58
58
  | `storage` | Three tables on the plugin's own migration track, named `plugin_pokemon_<name>` by the host: the companion row per key, the Dex, and the grant ledger. |
59
- | `files` | The species index and cached sprites live in the plugin's scoped data directory, **not** in a table. That directory is excluded from database snapshots, exactly as `request_bodies/` is — the alternative would put tens of megabytes of artwork into every snapshot an operator downloads, and it re-fetches itself anyway. |
60
- | `net:outbound` | Species data and sprites are fetched at runtime. The manifest also declares the origins, `https://pokeapi.co` and `https://raw.githubusercontent.com`; the host hands the plugin a `fetch` bound to that allowlist and refuses anything else. |
59
+ | `files` | The species index and cached sprites — companions and item icons alike — live in the plugin's scoped data directory, **not** in a table. That directory is excluded from database snapshots, exactly as `request_bodies/` is — the alternative would put tens of megabytes of artwork into every snapshot an operator downloads, and it re-fetches itself anyway. |
60
+ | `net:outbound` | Species data, companion sprites and item icons are fetched at runtime. The manifest also declares the origins, `https://pokeapi.co` and `https://raw.githubusercontent.com`; the host hands the plugin a `fetch` bound to that allowlist and refuses anything else. |
61
61
  | `events:request` | Growth is credited from `RequestCompleted` — all four token classes, which are disjoint, so summing them double-counts nothing. |
62
62
  | `events:limit` | A key parked at a `5h` or `1w` ceiling earns a rare candy, rated by the window's own length. A `1m` ceiling pays nothing: a minute is not a span in which work happened. |
63
63
 
@@ -68,8 +68,10 @@ accidental overreach is impossible and that the plugin's intent is auditable
68
68
  from one readable file. It constrains honest code and not hostile code.
69
69
 
70
70
  The plugin degrades rather than failing when a capability is absent. With no
71
- `net`, an incubating egg holds its progress instead of losing it, and the sprite
72
- route answers `503`.
71
+ `net`, an incubating egg holds its progress instead of losing it, and both
72
+ sprite routes answer `503` — the panel then draws each item as an emoji, which
73
+ is the same thing it draws before the cache has filled and the same thing it
74
+ draws for `mint`, whose icon does not exist upstream at all.
73
75
 
74
76
  ## Nintendo and Game Freak intellectual property
75
77
 
package/omni-plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "pokemon",
3
3
  "name": "Pok\u00e9mon Companion",
4
- "version": "1.0.1",
4
+ "version": "1.0.2",
5
5
  "api": 1,
6
6
  "sdk": "^0.1.0",
7
7
  "server": "server/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnigateway/pokemon",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A Pokémon companion for OmniGateway. Each gateway key raises one that hatches, evolves, and graduates into a Pokédex on the tokens that key spends.",
5
5
  "license": "MIT",
6
6
  "author": "Harismawan <mail@harismawan.com>",
package/server/index.js CHANGED
@@ -43,9 +43,26 @@ var RARE_CANDY_XP = 1e8;
43
43
  var ITEM_PRICES = {
44
44
  rareCandy: 500000000,
45
45
  mint: 1e8,
46
- shinyCharm: 3000000000
46
+ shinyCharm: 3000000000,
47
+ everstone: 1e9,
48
+ lure: 1e9,
49
+ sootheBell: 3000000000,
50
+ incense: 500000000,
51
+ repel: 500000000
47
52
  };
53
+ var SOOTHE_BONUS = 0.25;
48
54
  var ITEM_KINDS = Object.keys(ITEM_PRICES);
55
+ var ITEM_SPRITE_FILES = {
56
+ rareCandy: "rare-candy",
57
+ shinyCharm: "shiny-charm",
58
+ everstone: "everstone",
59
+ sootheBell: "soothe-bell",
60
+ repel: "repel",
61
+ incense: "luck-incense",
62
+ lure: "honey",
63
+ egg: "lucky-egg"
64
+ };
65
+ var ITEM_SPRITE_NAMES = new Map(Object.entries(ITEM_SPRITE_FILES));
49
66
  var FRESH_EGG_BASE_PRICE = 1e9;
50
67
  function freshEggPrice(tier) {
51
68
  if (tier === null)
@@ -99,12 +116,14 @@ function decideGrant(input) {
99
116
  var POKEAPI_ORIGIN = "https://pokeapi.co";
100
117
  var SPRITE_ORIGIN = "https://raw.githubusercontent.com";
101
118
  var SPRITE_DIR = "/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/animated";
119
+ var ITEM_SPRITE_DIR = "/PokeAPI/sprites/master/sprites/items";
102
120
  var MAX_EVOLUTION_CHAIN_ID = 2000;
103
121
  var INDEX_FETCH_CONCURRENCY = 8;
104
122
  var INDEX_PATH = "species/index.json";
105
123
  var speciesPath = (id) => `species/${id}.json`;
106
124
  var chainPath = (chainId) => `species/chain-${chainId}.json`;
107
125
  var spritePath = (id, shiny) => shiny ? `sprites/shiny/${id}.gif` : `sprites/${id}.gif`;
126
+ var itemSpritePath = (name) => `sprites/items/${name}.png`;
108
127
  function isFetchableSpeciesId(id) {
109
128
  return Number.isInteger(id) && hasAnimatedSprite(id);
110
129
  }
@@ -348,6 +367,10 @@ async function loadDetail(deps, id, chains) {
348
367
  function speciesDetail(deps, id) {
349
368
  return loadDetail(deps, id, new Map);
350
369
  }
370
+ function speciesDetails(deps, ids) {
371
+ const chains = new Map;
372
+ return Promise.all(ids.map((id) => loadDetail(deps, id, chains)));
373
+ }
351
374
  async function cachedSpeciesName(deps, id) {
352
375
  if (!isFetchableSpeciesId(id))
353
376
  return null;
@@ -427,6 +450,22 @@ async function spriteBytes(deps, id, shiny) {
427
450
  await writeCache(deps, path, bytes);
428
451
  return bytes;
429
452
  }
453
+ async function itemSpriteBytes(deps, item) {
454
+ const name = ITEM_SPRITE_NAMES.get(item);
455
+ if (name === undefined)
456
+ return null;
457
+ const path = itemSpritePath(name);
458
+ try {
459
+ const cached = await deps.files.read(path);
460
+ if (cached !== null && cached.length > 0)
461
+ return cached;
462
+ } catch {}
463
+ const bytes = await fetchBytes(deps, `${SPRITE_ORIGIN}${ITEM_SPRITE_DIR}/${name}.png`);
464
+ if (bytes === null)
465
+ return null;
466
+ await writeCache(deps, path, bytes);
467
+ return bytes;
468
+ }
430
469
 
431
470
  // src/roll.ts
432
471
  var NATURES = [
@@ -467,23 +506,33 @@ function mulberry32(seed) {
467
506
  };
468
507
  }
469
508
  var COLLECTED_WEIGHT = 0.25;
509
+ var FORM_WEIGHT = 0.6;
470
510
  function roll(input) {
471
511
  const random = mulberry32(input.seed);
472
- const eligible = input.candidates.filter((candidate) => {
512
+ const eligibleWith = (withLure) => input.candidates.filter((candidate) => {
473
513
  if (!hasAnimatedSprite(candidate.id))
474
514
  return false;
475
515
  if (candidate.id === DITTO_SPECIES_ID)
476
516
  return false;
517
+ if (input.excludeFinal != null && candidate.finalId === input.excludeFinal)
518
+ return false;
519
+ if (withLure && input.collectedFinals.has(candidate.finalId))
520
+ return false;
477
521
  if (input.guarantee === null)
478
522
  return true;
479
523
  const rarity2 = rarityFromCaptureRate(candidate.captureRate, false, false);
480
524
  return sortRank(rarity2) >= sortRank(input.guarantee);
481
525
  });
526
+ const wanted = input.onlyUncollected === true;
527
+ const lured = wanted ? eligibleWith(true) : [];
528
+ const usedLure = wanted && lured.length > 0;
529
+ const eligible = usedLure ? lured : eligibleWith(false);
482
530
  if (eligible.length === 0)
483
531
  return null;
484
532
  const weights = eligible.map((candidate) => {
485
533
  const base = Math.max(1, candidate.captureRate);
486
- return input.collectedFinals.has(candidate.finalId) ? base * COLLECTED_WEIGHT : base;
534
+ const collected = input.collectedFinals.has(candidate.finalId) ? base * COLLECTED_WEIGHT : base;
535
+ return input.preferLongLines === true ? collected * (1 + FORM_WEIGHT * (Math.max(1, candidate.forms) - 1)) : collected;
487
536
  });
488
537
  const total = weights.reduce((a, b) => a + b, 0);
489
538
  let target = random() * total;
@@ -502,10 +551,16 @@ function roll(input) {
502
551
  const rarity = rarityFromCaptureRate(chosen.captureRate, false, false);
503
552
  const dittoEligible = rarity === "common" && chosen.forms >= 2;
504
553
  const ditto = dittoEligible && random() < 1 / ODDS.dittoDisguise;
505
- return { speciesId: chosen.id, isShiny, nature, ditto };
554
+ return { speciesId: chosen.id, isShiny, nature, ditto, usedLure };
506
555
  }
507
556
 
508
557
  // src/state.ts
558
+ function emptyInventory() {
559
+ const inventory = {};
560
+ for (const kind of ITEM_KINDS)
561
+ inventory[kind] = 0;
562
+ return inventory;
563
+ }
509
564
  function freshState() {
510
565
  return {
511
566
  consumedTotal: 0,
@@ -513,7 +568,11 @@ function freshState() {
513
568
  eggUsage: 0,
514
569
  eggTier: null,
515
570
  pendingHatch: null,
516
- inventory: { rareCandy: 0, mint: 0, shinyCharm: 0 }
571
+ pendingReveal: null,
572
+ lure: false,
573
+ incense: false,
574
+ repel: null,
575
+ inventory: emptyInventory()
517
576
  };
518
577
  }
519
578
  function isRecord(value) {
@@ -534,7 +593,7 @@ function parseState(raw) {
534
593
  }
535
594
  if (!isRecord(parsed))
536
595
  return null;
537
- const inventory = { rareCandy: 0, mint: 0, shinyCharm: 0 };
596
+ const inventory = emptyInventory();
538
597
  const storedInventory = parsed.inventory;
539
598
  if (isRecord(storedInventory)) {
540
599
  for (const kind of ITEM_KINDS) {
@@ -562,7 +621,10 @@ function parseState(raw) {
562
621
  isShiny: storedActive.isShiny === true,
563
622
  nature: nature ?? "hardy",
564
623
  dittoDisguise: typeof storedActive.dittoDisguise === "number" ? storedActive.dittoDisguise : null,
565
- dittoRevealed: storedActive.dittoRevealed === true
624
+ dittoRevealed: storedActive.dittoRevealed === true,
625
+ everstone: storedActive.everstone === true,
626
+ soothe: storedActive.soothe === true,
627
+ soothedRaw: Math.max(0, asInt(storedActive.soothedRaw, 0))
566
628
  };
567
629
  }
568
630
  const storedPending = parsed.pendingHatch;
@@ -582,13 +644,28 @@ function parseState(raw) {
582
644
  };
583
645
  }
584
646
  }
647
+ const storedReveal = parsed.pendingReveal;
648
+ let pendingReveal = null;
649
+ if (isRecord(storedReveal)) {
650
+ const rarity = asRarity(storedReveal.rarity);
651
+ const path = Array.isArray(storedReveal.path) ? storedReveal.path.filter((id) => typeof id === "number" && id > 0) : [];
652
+ if (rarity !== null && path.length > 0)
653
+ pendingReveal = { path, rarity };
654
+ }
655
+ const storedConsumed = parsed.consumedTotal;
656
+ if (typeof storedConsumed !== "number" || !Number.isFinite(storedConsumed))
657
+ return null;
585
658
  const eggTier = asRarity(parsed.eggTier);
586
659
  return {
587
- consumedTotal: Math.max(0, asInt(parsed.consumedTotal, 0)),
660
+ consumedTotal: Math.max(0, Math.trunc(storedConsumed)),
588
661
  active,
589
662
  eggUsage: Math.max(0, asInt(parsed.eggUsage, 0)),
590
663
  eggTier: eggTier === null || eggTier === "legendary" ? null : eggTier,
591
664
  pendingHatch,
665
+ pendingReveal,
666
+ lure: parsed.lure === true,
667
+ incense: parsed.incense === true,
668
+ repel: typeof parsed.repel === "number" && Number.isInteger(parsed.repel) && parsed.repel > 0 ? parsed.repel : null,
592
669
  inventory
593
670
  };
594
671
  }
@@ -606,7 +683,19 @@ function advance(state, tokensTotal) {
606
683
  const events = [];
607
684
  let next = { ...state, consumedTotal: Math.trunc(tokensTotal) };
608
685
  if (gained > 0) {
609
- next = next.active === null ? { ...next, eggUsage: next.eggUsage + gained } : { ...next, active: { ...next.active, usedAtStage: next.active.usedAtStage + gained } };
686
+ const active = next.active;
687
+ if (active === null) {
688
+ next = { ...next, eggUsage: next.eggUsage + gained };
689
+ } else if (!active.soothe) {
690
+ next = { ...next, active: { ...active, usedAtStage: active.usedAtStage + gained } };
691
+ } else {
692
+ const raw = active.soothedRaw + gained;
693
+ const owed = Math.floor(raw * SOOTHE_BONUS) - Math.floor(active.soothedRaw * SOOTHE_BONUS);
694
+ next = {
695
+ ...next,
696
+ active: { ...active, soothedRaw: raw, usedAtStage: active.usedAtStage + gained + owed }
697
+ };
698
+ }
610
699
  }
611
700
  for (let step = 0;step < MAX_TRANSITIONS_PER_ADVANCE; step++) {
612
701
  if (next.active === null) {
@@ -624,7 +713,10 @@ function advance(state, tokensTotal) {
624
713
  isShiny: hatch.isShiny,
625
714
  nature: hatch.nature,
626
715
  dittoDisguise: hatch.ditto ? hatch.speciesId : null,
627
- dittoRevealed: false
716
+ dittoRevealed: false,
717
+ everstone: false,
718
+ soothe: false,
719
+ soothedRaw: 0
628
720
  };
629
721
  events.push({
630
722
  kind: "hatched",
@@ -636,10 +728,36 @@ function advance(state, tokensTotal) {
636
728
  continue;
637
729
  }
638
730
  const mon = next.active;
731
+ if (mon.everstone)
732
+ break;
639
733
  const needed = phaseThreshold(mon.rarity, mon.plannedPath.length, mon.stageIndex);
640
734
  if (mon.usedAtStage < needed)
641
735
  break;
642
736
  const excess = mon.usedAtStage - needed;
737
+ if (mon.dittoDisguise !== null && !mon.dittoRevealed) {
738
+ if (next.pendingReveal === null)
739
+ break;
740
+ const reveal = next.pendingReveal;
741
+ events.push({
742
+ kind: "revealed",
743
+ disguisedAs: mon.plannedPath[mon.stageIndex] ?? mon.baseId,
744
+ speciesId: reveal.path[0]
745
+ });
746
+ next = {
747
+ ...next,
748
+ active: {
749
+ ...mon,
750
+ baseId: reveal.path[0],
751
+ plannedPath: reveal.path,
752
+ stageIndex: 0,
753
+ usedAtStage: excess,
754
+ rarity: reveal.rarity,
755
+ dittoRevealed: true
756
+ },
757
+ pendingReveal: null
758
+ };
759
+ continue;
760
+ }
643
761
  if (mon.stageIndex < mon.plannedPath.length - 1) {
644
762
  events.push({
645
763
  kind: "evolved",
@@ -832,10 +950,16 @@ function consume(storage, apiKeyId, item, applyToState, now) {
832
950
  return { ok: false, reason: "unreadable" };
833
951
  if ((row.state.inventory[item] ?? 0) <= 0)
834
952
  return { ok: false, reason: "none-held" };
835
- const nextState = applyToState({
836
- ...row.state,
837
- inventory: { ...row.state.inventory, [item]: (row.state.inventory[item] ?? 0) - 1 }
838
- });
953
+ const outcome = applyToState(row.state);
954
+ if ("refused" in outcome)
955
+ return { ok: false, reason: outcome.refused };
956
+ const nextState = {
957
+ ...outcome.applied,
958
+ inventory: {
959
+ ...outcome.applied.inventory,
960
+ [item]: (outcome.applied.inventory[item] ?? 0) - 1
961
+ }
962
+ };
839
963
  storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
840
964
  serialiseState(nextState),
841
965
  now,
@@ -853,7 +977,10 @@ function purchase(storage, apiKeyId, entry, applyToState, now) {
853
977
  return { ok: false, reason: "unreadable" };
854
978
  if (wallet(row) < price)
855
979
  return { ok: false, reason: "insufficient" };
856
- const nextState = applyToState(row.state);
980
+ const outcome = applyToState(row.state);
981
+ if ("refused" in outcome)
982
+ return { ok: false, reason: outcome.refused };
983
+ const nextState = outcome.applied;
857
984
  storage.run("UPDATE {{companion}} SET state = ?, tokens_spent = tokens_spent + ?, updated_at = ? WHERE api_key_id = ?", [serialiseState(nextState), price, now, apiKeyId]);
858
985
  return {
859
986
  ok: true,
@@ -864,6 +991,9 @@ function purchase(storage, apiKeyId, entry, applyToState, now) {
864
991
 
865
992
  // src/server.ts
866
993
  var MAX_MULTIPLIER = 1000;
994
+ var WARM_PER_POLL = 8;
995
+ var WARM_BACKOFF_MS = 60000;
996
+ var WARM_BACKOFF_MAX_MS = 60 * 60000;
867
997
  function multiplierFrom(config) {
868
998
  const raw = config.multiplier;
869
999
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
@@ -890,7 +1020,7 @@ var server_default = definePlugin({
890
1020
  const existing = inFlight.get(apiKeyId);
891
1021
  if (existing !== undefined)
892
1022
  return existing;
893
- const started = prefetchHatch(apiKeyId, state).finally(() => inFlight.delete(apiKeyId));
1023
+ const started = prefetchHatch(apiKeyId, state).then(() => prefetchReveal(apiKeyId, state)).finally(() => inFlight.delete(apiKeyId));
894
1024
  inFlight.set(apiKeyId, started);
895
1025
  return started;
896
1026
  };
@@ -906,6 +1036,41 @@ var server_default = definePlugin({
906
1036
  names.set(speciesId, found);
907
1037
  return found;
908
1038
  };
1039
+ const warming = new Set;
1040
+ const cold = new Map;
1041
+ const warmNames = (ids) => {
1042
+ if (net === undefined || files === undefined)
1043
+ return;
1044
+ const now = ctx.now();
1045
+ const batch = [];
1046
+ for (const id of ids) {
1047
+ if (batch.length >= WARM_PER_POLL)
1048
+ break;
1049
+ if (names.has(id) || warming.has(id))
1050
+ continue;
1051
+ const chilled = cold.get(id);
1052
+ if (chilled !== undefined && now < chilled.until)
1053
+ continue;
1054
+ batch.push(id);
1055
+ warming.add(id);
1056
+ }
1057
+ if (batch.length === 0)
1058
+ return;
1059
+ speciesDetails({ net, files }, batch).then((details) => {
1060
+ batch.forEach((id, index) => {
1061
+ if (details[index]?.names.en !== undefined) {
1062
+ cold.delete(id);
1063
+ return;
1064
+ }
1065
+ const failures = (cold.get(id)?.failures ?? 0) + 1;
1066
+ const wait = Math.min(WARM_BACKOFF_MS * 2 ** (failures - 1), WARM_BACKOFF_MAX_MS);
1067
+ cold.set(id, { until: now + wait, failures });
1068
+ });
1069
+ }).catch(() => {}).finally(() => {
1070
+ for (const id of batch)
1071
+ warming.delete(id);
1072
+ });
1073
+ };
909
1074
  const settleAndRecord = (apiKeyId) => {
910
1075
  const result = settle(storage, apiKeyId, ctx.now());
911
1076
  if (result === null)
@@ -925,6 +1090,37 @@ var server_default = definePlugin({
925
1090
  ctx.logger.info("companion graduated", { event: "companion.graduated", count: 1 });
926
1091
  }
927
1092
  };
1093
+ const prefetchReveal = async (apiKeyId, state) => {
1094
+ const mon = state.active;
1095
+ if (mon === null || mon.dittoDisguise === null || mon.dittoRevealed)
1096
+ return;
1097
+ if (state.pendingReveal !== null)
1098
+ return;
1099
+ if (net === undefined || files === undefined)
1100
+ return;
1101
+ const detail = await speciesDetail({ net, files }, DITTO_SPECIES_ID);
1102
+ if (detail === null)
1103
+ return;
1104
+ const current = readCompanion(storage, apiKeyId);
1105
+ if (current?.state == null)
1106
+ return;
1107
+ const latest = current.state.active;
1108
+ if (latest === null || latest.dittoDisguise === null || latest.dittoRevealed)
1109
+ return;
1110
+ if (current.state.pendingReveal !== null)
1111
+ return;
1112
+ storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
1113
+ JSON.stringify({
1114
+ ...current.state,
1115
+ pendingReveal: {
1116
+ path: detail.chain,
1117
+ rarity: rarityFromCaptureRate(detail.captureRate, detail.isLegendary, detail.isMythical)
1118
+ }
1119
+ }),
1120
+ ctx.now(),
1121
+ apiKeyId
1122
+ ]);
1123
+ };
928
1124
  const prefetchHatch = async (apiKeyId, state) => {
929
1125
  if (state.active !== null || state.pendingHatch !== null)
930
1126
  return;
@@ -939,7 +1135,10 @@ var server_default = definePlugin({
939
1135
  seed: hashSeed(`${apiKeyId}:${state.consumedTotal}`),
940
1136
  guarantee: state.eggTier,
941
1137
  hasShinyCharm: hasShinyCharm(state),
942
- collectedFinals: collected
1138
+ collectedFinals: collected,
1139
+ onlyUncollected: state.lure,
1140
+ preferLongLines: state.incense,
1141
+ excludeFinal: state.repel
943
1142
  });
944
1143
  if (rolled === null)
945
1144
  return;
@@ -951,6 +1150,10 @@ var server_default = definePlugin({
951
1150
  const current = readCompanion(storage, apiKeyId);
952
1151
  if (current?.state == null || current.state.pendingHatch !== null)
953
1152
  return;
1153
+ if (current.state.active !== null)
1154
+ return;
1155
+ if (paidRollInputs(current.state) !== paidRollInputs(state))
1156
+ return;
954
1157
  storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
955
1158
  JSON.stringify({
956
1159
  ...current.state,
@@ -961,7 +1164,10 @@ var server_default = definePlugin({
961
1164
  isShiny: rolled.isShiny,
962
1165
  nature: rolled.nature,
963
1166
  ditto: rolled.ditto
964
- }
1167
+ },
1168
+ lure: state.lure && !rolled.usedLure,
1169
+ incense: false,
1170
+ repel: null
965
1171
  }),
966
1172
  ctx.now(),
967
1173
  apiKeyId
@@ -1046,14 +1252,21 @@ var server_default = definePlugin({
1046
1252
  prefetchOnce(apiKeyId, row.state).catch(() => {});
1047
1253
  const active = row.state?.active ?? null;
1048
1254
  const dex = readDex(storage, apiKeyId);
1255
+ const stageId = active === null ? null : active.plannedPath[active.stageIndex] ?? null;
1256
+ const stageName = await nameOf(stageId);
1257
+ const named = await Promise.all(dex.map(async (entry) => ({ ...entry, name: await nameOf(entry.finalId) })));
1258
+ warmNames([
1259
+ ...stageId !== null && stageName === null ? [stageId] : [],
1260
+ ...named.filter((entry) => entry.name === null).map((entry) => entry.finalId)
1261
+ ]);
1049
1262
  return {
1050
1263
  json: {
1051
1264
  state: row.state,
1052
1265
  tokensTotal: row.tokensTotal,
1053
1266
  wallet: wallet(row),
1054
1267
  lastCreditAt: row.lastCreditAt,
1055
- name: await nameOf(active === null ? null : active.plannedPath[active.stageIndex] ?? null),
1056
- dex: await Promise.all(dex.map(async (entry) => ({ ...entry, name: await nameOf(entry.finalId) }))),
1268
+ name: stageName,
1269
+ dex: named,
1057
1270
  shop: shopCatalogue(),
1058
1271
  nextThreshold: active === null ? EGG_HATCH_THRESHOLD : phaseThreshold(active.rarity, active.plannedPath.length, active.stageIndex),
1059
1272
  progress: active === null ? row.state?.eggUsage ?? 0 : active.usedAtStage
@@ -1083,6 +1296,27 @@ var server_default = definePlugin({
1083
1296
  };
1084
1297
  }
1085
1298
  },
1299
+ {
1300
+ method: "GET",
1301
+ path: "/item-sprite/:item",
1302
+ handler: async (request) => {
1303
+ const item = request.params.item ?? "";
1304
+ if (!ITEM_SPRITE_NAMES.has(item)) {
1305
+ return { status: 404, json: { error: "no item icon" } };
1306
+ }
1307
+ if (net === undefined || files === undefined) {
1308
+ return { status: 503, json: { error: "sprites need the net and files capabilities" } };
1309
+ }
1310
+ const bytes = await itemSpriteBytes({ net, files }, item);
1311
+ if (bytes === null)
1312
+ return { status: 404, json: { error: "no item icon" } };
1313
+ return {
1314
+ bytes,
1315
+ contentType: "image/png",
1316
+ cacheControl: "public, max-age=31536000, immutable"
1317
+ };
1318
+ }
1319
+ },
1086
1320
  {
1087
1321
  method: "POST",
1088
1322
  path: "/keys/:id/use",
@@ -1098,6 +1332,30 @@ var server_default = definePlugin({
1098
1332
  return { json: { ok: true } };
1099
1333
  }
1100
1334
  },
1335
+ {
1336
+ method: "POST",
1337
+ path: "/keys/:id/unpin",
1338
+ handler: (request) => {
1339
+ const apiKeyId = request.params.id ?? "";
1340
+ const row = readCompanion(storage, apiKeyId);
1341
+ if (row === null)
1342
+ return { status: 404, json: { error: "no companion for that key" } };
1343
+ if (row.state === null)
1344
+ return { status: 409, json: { error: "unreadable" } };
1345
+ const active = row.state.active;
1346
+ if (active === null)
1347
+ return { status: 409, json: { error: "no-companion" } };
1348
+ if (!active.everstone)
1349
+ return { status: 409, json: { error: "nothing-new" } };
1350
+ storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
1351
+ JSON.stringify({ ...row.state, active: { ...active, everstone: false } }),
1352
+ ctx.now(),
1353
+ apiKeyId
1354
+ ]);
1355
+ settleAndRecord(apiKeyId);
1356
+ return { json: { ok: true } };
1357
+ }
1358
+ },
1101
1359
  {
1102
1360
  method: "POST",
1103
1361
  path: "/keys/:id/purchase",
@@ -1116,6 +1374,15 @@ var server_default = definePlugin({
1116
1374
  return { routes };
1117
1375
  }
1118
1376
  });
1377
+ function paidRollInputs(state) {
1378
+ return JSON.stringify([
1379
+ state.eggTier,
1380
+ hasShinyCharm(state),
1381
+ state.lure,
1382
+ state.incense,
1383
+ state.repel
1384
+ ]);
1385
+ }
1119
1386
  function hashSeed(input) {
1120
1387
  let hash = 2166136261;
1121
1388
  for (let i = 0;i < input.length; i++) {
@@ -1136,7 +1403,7 @@ function shopCatalogue() {
1136
1403
  price: freshEggPrice("uncommon")
1137
1404
  },
1138
1405
  { entry: { kind: "egg", tier: "rare" }, price: freshEggPrice("rare") }
1139
- ];
1406
+ ].sort((a, b) => a.price - b.price);
1140
1407
  }
1141
1408
  function parseShopEntry(body) {
1142
1409
  if (typeof body !== "object" || body === null)
@@ -1158,32 +1425,86 @@ function parseShopEntry(body) {
1158
1425
  }
1159
1426
  function applyPurchase(state, entry) {
1160
1427
  if (entry.kind === "egg") {
1161
- return { ...state, active: null, eggUsage: 0, eggTier: entry.tier, pendingHatch: null };
1428
+ if (state.active === null)
1429
+ return { refused: "no-companion" };
1430
+ return {
1431
+ applied: { ...state, active: null, eggUsage: 0, eggTier: entry.tier, pendingHatch: null }
1432
+ };
1433
+ }
1434
+ if (!HELD_ITEMS.includes(entry.item) && (state.inventory[entry.item] ?? 0) > 0) {
1435
+ return { refused: "already-owned" };
1162
1436
  }
1163
1437
  return {
1164
- ...state,
1165
- inventory: { ...state.inventory, [entry.item]: (state.inventory[entry.item] ?? 0) + 1 }
1438
+ applied: {
1439
+ ...state,
1440
+ inventory: { ...state.inventory, [entry.item]: (state.inventory[entry.item] ?? 0) + 1 }
1441
+ }
1166
1442
  };
1167
1443
  }
1444
+ var HELD_ITEMS = [
1445
+ "rareCandy",
1446
+ "mint",
1447
+ "everstone",
1448
+ "lure",
1449
+ "sootheBell",
1450
+ "incense",
1451
+ "repel"
1452
+ ];
1168
1453
  function parseHeldItem(body) {
1169
1454
  if (typeof body !== "object" || body === null)
1170
1455
  return null;
1171
1456
  const item = body.item;
1172
- return item === "rareCandy" || item === "mint" ? item : null;
1457
+ return HELD_ITEMS.includes(item) ? item : null;
1173
1458
  }
1174
1459
  function useItem(state, item) {
1460
+ if (item === "everstone" || item === "sootheBell" || item === "repel") {
1461
+ const active = state.active;
1462
+ if (active === null)
1463
+ return { refused: "no-companion" };
1464
+ if (item === "everstone") {
1465
+ if (active.everstone)
1466
+ return { refused: "nothing-new" };
1467
+ return { applied: { ...state, active: { ...active, everstone: true } } };
1468
+ }
1469
+ if (item === "sootheBell") {
1470
+ if (active.soothe)
1471
+ return { refused: "nothing-new" };
1472
+ return { applied: { ...state, active: { ...active, soothe: true } } };
1473
+ }
1474
+ const finalId = active.plannedPath[active.plannedPath.length - 1];
1475
+ if (finalId === undefined)
1476
+ return { refused: "no-companion" };
1477
+ if (finalId === DITTO_SPECIES_ID)
1478
+ return { refused: "nothing-new" };
1479
+ if (state.repel !== null)
1480
+ return { refused: "already-armed" };
1481
+ return { applied: { ...state, repel: finalId } };
1482
+ }
1483
+ if (item === "lure" || item === "incense") {
1484
+ if (item === "lure") {
1485
+ if (state.lure)
1486
+ return { refused: "nothing-new" };
1487
+ return { applied: { ...state, lure: true } };
1488
+ }
1489
+ if (state.incense)
1490
+ return { refused: "nothing-new" };
1491
+ return { applied: { ...state, incense: true } };
1492
+ }
1175
1493
  if (item === "mint") {
1176
1494
  if (state.active === null)
1177
- return state;
1495
+ return { refused: "no-companion" };
1178
1496
  const index = NATURES.indexOf(state.active.nature);
1179
1497
  const nature = NATURES[(index + 1) % NATURES.length];
1180
- return { ...state, active: { ...state.active, nature } };
1498
+ return { applied: { ...state, active: { ...state.active, nature } } };
1181
1499
  }
1182
- return state.active === null ? { ...state, eggUsage: state.eggUsage + RARE_CANDY_XP } : {
1183
- ...state,
1184
- active: { ...state.active, usedAtStage: state.active.usedAtStage + RARE_CANDY_XP }
1500
+ return {
1501
+ applied: state.active === null ? { ...state, eggUsage: state.eggUsage + RARE_CANDY_XP } : {
1502
+ ...state,
1503
+ active: { ...state.active, usedAtStage: state.active.usedAtStage + RARE_CANDY_XP }
1504
+ }
1185
1505
  };
1186
1506
  }
1187
1507
  export {
1508
+ HELD_ITEMS,
1188
1509
  server_default as default
1189
1510
  };
package/ui/index.js CHANGED
@@ -1,10 +1,10 @@
1
- function V(e){return e}import{useMutation as we,useQuery as ve,useQueryClient as De}from"@tanstack/react-query";import{useState as Pe}from"react";function w(e,r,i){return`/api/plugins/${e}/sprite/${r}${i?"?shiny=1":""}`}function c(e){if(e>=1e9)return`${(e/1e9).toFixed(2)}B`;if(e>=1e6)return`${(e/1e6).toFixed(1)}M`;return e.toLocaleString()}function v(e,r){return e??`#${r}`}function A(e,r,i){return`${e??`Species ${r}`}${i?", shiny":""}`}function R(e){return e.replace(/([A-Z])/g," $1").toLowerCase()}function F(e){if(e.kind==="item")return R(e.item);return e.tier===null?"fresh egg":`fresh egg (${e.tier}+)`}var te=["rareCandy","mint"],ne=[null,"common","uncommon","rare","legendary"],re=60000,ee=60*re;function T(e,r,i){if(!e)return"egg";if(r===null)return"sleep";let o=i-r;if(o<5*re)return"working";if(o<ee)return"idle";if(o<8*ee)return"tired";return"sleep"}import a from"styled-components";var l={xs:"4px",sm:"8px",md:"12px",lg:"20px",xl:"32px"},H="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",I=a.section`
1
+ function ue(e){return e}import{useMutation as xe,useQuery as at,useQueryClient as vt}from"@tanstack/react-query";import{useState as st}from"react";var pt={rareCandy:{blurb:"Injects 100.0M growth. Priced at five times what it grants.",emoji:"\uD83C\uDF6C",consumable:!0},mint:{blurb:"Rerolls this companion's nature. Cosmetic, and cheap enough to reroll again.",emoji:"\uD83C\uDF3F",consumable:!0},shinyCharm:{blurb:"Kept, never spent. Raises every future hatch from 1 in 64 to 1 in 48.",emoji:"✨",consumable:!1},everstone:{blurb:"Pins this companion: it will not evolve, reveal, or graduate.",emoji:"\uD83E\uDEA8",consumable:!0},lure:{blurb:"Next egg prefers a species the Dex has not collected. A preference, never a veto.",emoji:"\uD83C\uDF6F",consumable:!0},sootheBell:{blurb:"This companion grows 25% faster. Bound to it, and never repays its price.",emoji:"\uD83D\uDD14",consumable:!0},incense:{blurb:"Next egg leans toward a longer evolution line. Buys events, not value.",emoji:"\uD83D\uDD6F️",consumable:!0},repel:{blurb:"Next egg will not hatch one named line.",emoji:"\uD83D\uDEAB",consumable:!0}};function N(e){return ye.get(e)??{blurb:"",emoji:"❔",consumable:!0}}var ye=new Map(Object.entries(pt)),Gt=[...ye].filter(([,e])=>e.consumable).map(([e])=>e),ke="\uD83E\uDD5A";function Se(e){return e===null?"Sends this companion off and starts again.":`Sends this companion off for an egg guaranteed to hatch ${e} or better.`}function T(e,t,o){return`/api/plugins/${e}/sprite/${t}${o?"?shiny=1":""}`}function Ie(e,t){return`/api/plugins/${e}/item-sprite/${t}`}function h(e){if(e>=1e9)return`${(e/1e9).toFixed(2)}B`;if(e>=1e6)return`${(e/1e6).toFixed(1)}M`;return e.toLocaleString()}function R(e,t){return e??`#${t}`}function P(e,t,o){return`${e??`Species ${t}`}${o?", shiny":""}`}function L(e){return e.replace(/([A-Z])/g," $1").toLowerCase()}function we(e){if(e.kind==="item")return L(e.item);return e.tier===null?"fresh egg":`fresh egg (${e.tier}+)`}var $e=[null,"common","uncommon","rare","legendary"],Ce=60000,ve=60*Ce;function _(e,t,o){if(!e)return"egg";if(t===null)return"sleep";let a=o-t;if(a<5*Ce)return"working";if(a<ve)return"idle";if(a<8*ve)return"tired";return"sleep"}import{useState as ut}from"react";import i from"styled-components";var l={xs:"4px",sm:"8px",md:"12px",lg:"20px",xl:"32px"},B="192px",de="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",M=i.section`
2
2
  background: var(--panel);
3
3
  border: 1px solid var(--rule);
4
4
  border-radius: 8px;
5
5
  padding: ${l.lg};
6
6
  color: var(--ink);
7
- `,P=a.h3`
7
+ `,ce=i.h3`
8
8
  display: flex;
9
9
  align-items: center;
10
10
  gap: ${l.md};
@@ -21,20 +21,83 @@ function V(e){return e}import{useMutation as we,useQuery as ve,useQueryClient as
21
21
  height: 1px;
22
22
  background: var(--rule);
23
23
  }
24
- `,y=a.div`
24
+ `,Te=i(ce)`
25
+ &::after {
26
+ content: none;
27
+ }
28
+ `,Re=i.span`
29
+ flex: 1;
30
+ height: 1px;
31
+ background: var(--rule);
32
+ `,Pe=i.button`
33
+ display: flex;
34
+ align-items: center;
35
+ gap: ${l.sm};
36
+ flex: 1;
37
+ padding: 0;
38
+ background: none;
39
+ border: 0;
40
+ color: inherit;
41
+ font: inherit;
42
+ letter-spacing: inherit;
43
+ text-transform: inherit;
44
+ text-align: left;
45
+ cursor: pointer;
46
+
47
+ &:hover {
48
+ color: var(--ink);
49
+ }
50
+ &:focus-visible {
51
+ outline: 2px solid var(--accent);
52
+ outline-offset: 4px;
53
+ border-radius: 2px;
54
+ }
55
+ `,Ae=i.span`
56
+ display: inline-block;
57
+ font-size: 9px;
58
+ line-height: 1;
59
+ transform: rotate(${(e)=>e.$open?"90deg":"0deg"});
60
+ transition: transform 120ms ease-out;
61
+
62
+ @media (prefers-reduced-motion: reduce) {
63
+ transition: none;
64
+ }
65
+ `,Ee=i.span`
66
+ color: var(--ink-faint);
67
+ letter-spacing: 0.08em;
68
+ white-space: nowrap;
69
+ `,w=i.div`
25
70
  display: flex;
26
71
  gap: ${l.md};
27
72
  align-items: center;
28
73
  flex-wrap: wrap;
29
- `,p=a.span`
74
+ `,De=i(w)`
75
+ h2 {
76
+ margin: 0;
77
+ }
78
+ `,x=i.span`
79
+ color: var(--ink-dim);
80
+ `,F=i(x)`
81
+ display: block;
82
+ margin-top: ${l.sm};
83
+
84
+ /* Two facts about one meter are closer to each other than either is to the
85
+ meter, because that is what they are: "stage 1 of 2" and the tokens under
86
+ it are one reading, not two. Spaced evenly they read as a list of unrelated
87
+ numbers that happen to be stacked. */
88
+ & + & {
89
+ margin-top: ${l.xs};
90
+ }
91
+ `,Ne=i.p`
30
92
  color: var(--ink-dim);
31
- `,G=a.p`
93
+ margin: ${l.xs} 0 ${l.lg};
94
+ `,ge=i.p`
32
95
  color: var(--warn);
33
96
  background: var(--warn-wash);
34
97
  border-radius: 6px;
35
98
  padding: ${l.md};
36
99
  margin: ${l.md} 0 0;
37
- `,b=a.span`
100
+ `,b=i.span`
38
101
  display: inline-flex;
39
102
  align-items: center;
40
103
  gap: ${l.xs};
@@ -46,30 +109,31 @@ function V(e){return e}import{useMutation as we,useQuery as ve,useQueryClient as
46
109
  text-transform: uppercase;
47
110
  color: var(--ink-dim);
48
111
  white-space: nowrap;
49
- `,ie=a(b)`
112
+ `,Le=i(w)`
113
+ margin: ${l.xs} 0 ${l.sm};
114
+ `,j=i(b)`
50
115
  border-color: var(--rule-strong);
51
116
  color: var(--ink);
52
117
  font-weight: 600;
53
- `,C=a.img`
54
- width: 96px;
55
- height: 96px;
118
+ `,z=i.img`
119
+ width: ${B};
120
+ height: ${B};
56
121
  image-rendering: pixelated;
57
- background: var(--panel-sunk);
58
- border: 1px solid var(--rule);
59
- border-radius: 6px;
60
- `,D=a.div`
61
- width: 96px;
62
- height: 96px;
122
+ `,q=i.div`
123
+ box-sizing: border-box;
124
+ width: ${B};
125
+ height: ${B};
63
126
  border-radius: 50% 50% 45% 45%;
64
127
  background: var(--panel-sunk);
65
128
  border: 2px solid var(--rule-strong);
66
- `,oe=a.div`
67
- width: 96px;
68
- height: 96px;
129
+ `,Be=i.div`
130
+ box-sizing: border-box;
131
+ width: ${B};
132
+ height: ${B};
69
133
  border-radius: 6px;
70
134
  background: var(--warn-wash);
71
135
  border: 2px dashed var(--warn);
72
- `,x=a.button`
136
+ `,v=i.button`
73
137
  background: var(--panel-raised);
74
138
  border: 1px solid var(--rule);
75
139
  border-radius: 6px;
@@ -94,22 +158,22 @@ function V(e){return e}import{useMutation as we,useQuery as ve,useQueryClient as
94
158
  color: var(--ink-faint);
95
159
  cursor: not-allowed;
96
160
  }
97
- `,B=a.span`
98
- font-family: ${H};
161
+ `,O=i.span`
162
+ font-family: ${de};
99
163
  font-variant-numeric: tabular-nums;
100
- `,ae=a.div`
164
+ `,Oe=i.div`
101
165
  display: flex;
102
166
  gap: 3px;
103
167
  min-width: 240px;
104
168
  max-width: 340px;
105
- `,se=a.div`
169
+ `,Ke=i.div`
106
170
  flex: 1;
107
171
  height: 10px;
108
172
  background: var(--panel-sunk);
109
173
  border: 1px solid var(--rule);
110
174
  border-radius: 3px;
111
175
  overflow: hidden;
112
- `,q=a.div`
176
+ `,me=i.div`
113
177
  height: 100%;
114
178
  background: var(--accent);
115
179
  width: ${(e)=>Math.min(100,Math.max(0,e.$pct))}%;
@@ -118,48 +182,94 @@ function V(e){return e}import{useMutation as we,useQuery as ve,useQueryClient as
118
182
  @media (prefers-reduced-motion: reduce) {
119
183
  transition: none;
120
184
  }
121
- `,le=a.dl`
185
+ `,_e=i.dl`
122
186
  display: flex;
123
187
  flex-wrap: wrap;
124
188
  gap: ${l.xl};
125
189
  margin: ${l.lg} 0 0;
126
190
  padding-top: ${l.lg};
127
191
  border-top: 1px solid var(--rule);
128
- `,N=a.div`
192
+ `,J=i.div`
129
193
  display: flex;
130
194
  flex-direction: column;
131
195
  gap: 2px;
132
- `,L=a.dt`
196
+ `,Q=i.dt`
133
197
  font-size: 11px;
134
198
  letter-spacing: 0.08em;
135
199
  text-transform: uppercase;
136
200
  color: var(--ink-faint);
137
- `,_=a.dd`
201
+ `,W=i.dd`
138
202
  margin: 0;
139
- font-family: ${H};
203
+ font-family: ${de};
140
204
  font-variant-numeric: tabular-nums;
141
205
  font-size: 18px;
142
206
  color: var(--ink);
143
- `,pe=a.div`
207
+ `,Me=i.div`
144
208
  display: grid;
145
209
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
146
210
  gap: ${l.md};
147
- `,ue=a.figure`
211
+ `,Fe=i.button`
212
+ margin: 0;
213
+ display: flex;
214
+ flex-direction: column;
215
+ align-items: center;
216
+ gap: 2px;
217
+ padding: ${l.xs};
218
+ background: ${(e)=>e.$open?"var(--accent-wash)":"none"};
219
+ border: 1px solid ${(e)=>e.$open?"var(--accent)":"transparent"};
220
+ border-radius: 8px;
221
+ color: inherit;
222
+ font: inherit;
223
+ cursor: pointer;
224
+
225
+ &:hover {
226
+ border-color: ${(e)=>e.$open?"var(--accent)":"var(--rule)"};
227
+ }
228
+ &:focus-visible {
229
+ outline: 2px solid var(--accent);
230
+ outline-offset: 2px;
231
+ }
232
+ `,Ue=i.div`
233
+ grid-column: 1 / -1;
234
+ display: flex;
235
+ gap: ${l.lg};
236
+ padding: ${l.md};
237
+ background: var(--panel-sunk);
238
+ border: 1px solid var(--rule);
239
+ border-radius: 8px;
240
+ `,Ge=i.div`
241
+ display: flex;
242
+ flex-direction: column;
243
+ align-items: flex-start;
244
+ gap: ${l.sm};
245
+ min-width: 0;
246
+ `,He=i.div`
247
+ display: flex;
248
+ align-items: center;
249
+ gap: ${l.sm};
250
+ flex-wrap: wrap;
251
+ `,Ve=i.figure`
148
252
  margin: 0;
149
253
  display: flex;
150
254
  flex-direction: column;
151
255
  align-items: center;
152
256
  gap: 2px;
153
- `,z=a.figcaption`
257
+
258
+ img {
259
+ width: 48px;
260
+ height: 48px;
261
+ image-rendering: pixelated;
262
+ }
263
+ `,Y=i.figcaption`
154
264
  color: var(--ink-dim);
155
265
  font-size: 11px;
156
266
  text-align: center;
157
267
  overflow-wrap: anywhere;
158
- `,de=a.div`
268
+ `,je=i.div`
159
269
  display: grid;
160
- grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
270
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
161
271
  gap: ${l.md};
162
- `,ce=a.button`
272
+ `,ze=i.button`
163
273
  display: flex;
164
274
  flex-direction: column;
165
275
  align-items: center;
@@ -180,16 +290,52 @@ function V(e){return e}import{useMutation as we,useQuery as ve,useQueryClient as
180
290
  outline: 2px solid var(--accent);
181
291
  outline-offset: 2px;
182
292
  }
183
- `,K=a.span`
184
- font-family: ${H};
293
+ `,Z=i.span`
294
+ font-family: ${de};
185
295
  font-size: 12px;
186
296
  color: var(--ink-dim);
187
297
  overflow-wrap: anywhere;
188
- `,ge=a.div`
298
+ `,X=i.div`
299
+ display: grid;
300
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
301
+ gap: ${l.md};
302
+ `,ee=i.div`
189
303
  display: flex;
190
- align-items: center;
304
+ flex-direction: column;
191
305
  gap: ${l.sm};
192
- padding: ${l.sm} ${l.md};
306
+ padding: ${l.md};
193
307
  background: var(--panel-sunk);
194
- border-radius: 6px;
195
- `;import{jsx as U,jsxs as me}from"react/jsx-runtime";function fe({inventory:e,onUse:r,pending:i}){let o=Object.entries(e).filter(([,t])=>t>0);if(o.length===0)return U(p,{children:"Nothing in the bag."});return U(y,{children:o.map(([t,n])=>me(ge,{children:[U("span",{children:`${R(t)} · ${n}`}),te.includes(t)?me(x,{disabled:i,onClick:()=>r(t),type:"button",children:["Use ",R(t)]}):U(p,{children:"held"})]},t))})}import{useState as $e}from"react";import{jsx as S,jsxs as J,Fragment as Ie}from"react/jsx-runtime";function he({entries:e,pluginId:r}){let[i,o]=$e(null);if(e.length===0)return S(p,{children:"Nothing graduated yet."});let t=i===null?e:e.filter((n)=>n.rarity===i);return J(Ie,{children:[S(y,{children:ne.map((n)=>S(x,{"aria-pressed":i===n,onClick:()=>o(n),type:"button",children:n??"all"},n??"all"))}),t.length===0?J(p,{children:["No ",i," graduates yet."]}):S(pe,{children:t.map((n)=>J(ue,{children:[S("img",{alt:`${n.rarity}${n.isShiny?" shiny":""} ${n.name??`species ${n.finalId}`}`,src:w(r,n.finalId,n.isShiny),style:{width:"64px",height:"64px",imageRendering:"pixelated"}}),S(z,{children:v(n.name,n.finalId)}),n.nature===null?null:S(z,{children:n.nature})]},n.id))})]})}import{jsx as Q,jsxs as Ee}from"react/jsx-runtime";function W({stages:e,stageIndex:r,progress:i,threshold:o,label:t,valueText:n}){let f=i/Math.max(1,o)*100;return Q(ae,{"aria-label":t,"aria-valuemax":o,"aria-valuemin":0,"aria-valuenow":i,"aria-valuetext":n,role:"progressbar",children:Array.from({length:Math.max(1,e)},(s,k)=>Ee(se,{children:[k<r?Q(q,{$pct:100}):null,k===r?Q(q,{$pct:f}):null]},k))})}function ye(e,r,i,o){return`stage ${r+1} of ${e}, ${c(i)} of ${c(o)} tokens`}import{jsx as g,jsxs as m,Fragment as M}from"react/jsx-runtime";function xe({view:e,activity:r,pluginId:i}){let o=e.state;if(o===null)return null;let{active:t}=o,n=t===null?null:t.plannedPath[t.stageIndex]??null;return m(M,{children:[m(y,{children:[n===null?g(D,{"aria-label":"An egg, not yet hatched",role:"img"}):g(C,{alt:A(e.name,n,t?.isShiny===!0),src:w(i,n,t?.isShiny===!0)}),m("div",{children:[g("h3",{children:n===null?"Egg":v(e.name,n)}),m(y,{children:[t===null?o.eggTier===null?null:m(b,{children:[o.eggTier,"+ guaranteed"]}):m(M,{children:[g(b,{children:t.rarity}),t.isShiny?m(ie,{children:[g("span",{"aria-hidden":"true",children:"✦"}),"shiny"]}):null,g(b,{children:t.nature}),t.dittoDisguise===null?null:g(b,{children:"?"})]}),g(b,{"aria-label":`Activity: ${r}`,role:"status",children:r})]}),t===null?m(M,{children:[g(W,{label:"Incubation",progress:e.progress,stageIndex:0,stages:1,threshold:e.nextThreshold,valueText:`${c(e.progress)} of ${c(e.nextThreshold)} tokens incubated`}),m(p,{children:[c(e.progress)," / ",c(e.nextThreshold)," tokens incubated"]})]}):m(M,{children:[g(W,{label:"Growth to the next evolution",progress:e.progress,stageIndex:t.stageIndex,stages:t.plannedPath.length,threshold:e.nextThreshold,valueText:ye(t.plannedPath.length,t.stageIndex,e.progress,e.nextThreshold)}),m(p,{children:["Stage ",t.stageIndex+1," of ",t.plannedPath.length]}),m(p,{children:[c(e.progress)," / ",c(e.nextThreshold)," to the next stage"]})]})]})]}),m(le,{children:[m(N,{children:[g(L,{children:"Earned"}),g(_,{children:c(e.tokensTotal)})]}),m(N,{children:[g(L,{children:"To spend"}),g(_,{children:c(e.wallet)})]}),m(N,{children:[g(L,{children:"Graduated"}),g(_,{children:e.dex.length.toLocaleString()})]})]})]})}import{useState as Ae}from"react";import{jsx as u,jsxs as O}from"react/jsx-runtime";function be({keys:e,onPick:r,pluginId:i,rosterFailed:o}){let[t,n]=Ae("");return O(I,{children:[u("h2",{children:"Companion"}),u("p",{children:u(p,{children:"Each API key raises its own Pokémon on the tokens it spends. Pick a key."})}),e.length>0?u(de,{children:e.map((f)=>u(Re,{entry:f,onPick:r,pluginId:i},f.apiKeyId))}):u(p,{children:o?"The list of keys could not be loaded. Enter a key id below to reach a companion directly.":"No key has spent a token yet. A companion appears the first time a key serves a request."}),u(P,{children:"Or by key id"}),u("form",{onSubmit:(f)=>{f.preventDefault();let s=t.trim();if(s!=="")r(s)},children:O(y,{children:[u("input",{"aria-label":"API key id",onChange:(f)=>n(f.target.value),placeholder:"key id",value:t}),u(x,{type:"submit",children:"Show"})]})})]})}function Re({entry:e,onPick:r,pluginId:i}){let o=T(e.speciesId!==null,e.lastCreditAt,Date.now());return O(ce,{onClick:()=>r(e.apiKeyId),type:"button",children:[e.unreadable?u(oe,{"aria-label":"This key's save could not be read",role:"img"}):e.speciesId===null?u(D,{"aria-label":"An egg, not yet hatched",role:"img"}):u(C,{alt:A(e.name,e.speciesId,e.isShiny),src:w(i,e.speciesId,e.isShiny)}),u("strong",{children:e.unreadable?"Save unreadable":e.speciesId===null?"Egg":v(e.name,e.speciesId)}),e.rarity===null?null:u(b,{children:e.rarity}),u(K,{children:e.apiKeyId}),O(p,{children:[u(B,{children:c(e.tokensTotal)}),e.unreadable?null:` · ${o}`]})]})}import{jsx as ke,jsxs as Ce}from"react/jsx-runtime";function Se({offers:e,wallet:r,onBuy:i,pending:o}){return ke(y,{children:e.map((t)=>Ce(x,{disabled:r<t.price||o,onClick:()=>i(t.entry),type:"button",children:[F(t.entry)," · ",ke(B,{children:c(t.price)})]},`${t.entry.kind}:${F(t.entry)}`))})}import{jsx as d,jsxs as Y,Fragment as _e}from"react/jsx-runtime";var Be=15000;function Ne({pluginId:e,api:r}){let[i,o]=Pe({at:"start"}),t=De(),n=ve({queryKey:["roster"],queryFn:()=>r.get("keys")}),f=n.data?.keys??[];if(i.at==="start"&&!n.isPending){let h=f.length===1?f[0]:void 0;o(h===void 0?{at:"roster"}:{at:"key",apiKeyId:h.apiKeyId})}let s=i.at==="key"?i.apiKeyId:null,k=ve({queryKey:["companion",s],queryFn:()=>r.get(`keys/${s}`),enabled:s!==null,refetchInterval:Be}),[Te,E]=Pe(null),Z=()=>{t.invalidateQueries({queryKey:["companion",s]}),t.invalidateQueries({queryKey:["roster"]})},X=we({mutationFn:(h)=>r.post(`keys/${s}/purchase`,h),onMutate:()=>E(null),onError:(h)=>E(h instanceof Error?h.message:"the purchase was refused"),onSuccess:Z}),j=we({mutationFn:(h)=>r.post(`keys/${s}/use`,{item:h}),onMutate:()=>E(null),onError:(h)=>E(h instanceof Error?h.message:"the item could not be used"),onSuccess:Z});if(n.isPending)return d(I,{children:"Loading…"});if(s===null)return d(be,{keys:f,onPick:(h)=>o({at:"key",apiKeyId:h}),pluginId:e,rosterFailed:n.isError});return Y(I,{children:[Y(y,{children:[d("h2",{children:"Companion"}),d(K,{children:s}),d(x,{onClick:()=>o({at:"roster"}),type:"button",children:"All keys"})]}),d(Le,{buy:X.mutate,buying:X.isPending,keyId:s,onUse:j.mutate,pluginId:e,query:k,refusal:Te,using:j.isPending})]})}function Le({query:e,pluginId:r,buy:i,onUse:o,buying:t,using:n,refusal:f}){if(e.isPending)return d(p,{children:"Loading…"});if(e.isError||e.data===void 0)return d(p,{children:"No companion for that key yet."});let s=e.data;if(s.state===null)return d(G,{children:"This key's save could not be read. It has been left untouched rather than replaced — nothing has been lost, but it needs looking at."});let k=T(s.state.active!==null,s.lastCreditAt,Date.now());return Y(_e,{children:[d(xe,{activity:k,pluginId:r,view:s}),d(P,{children:"Shop"}),d(Se,{offers:s.shop,onBuy:i,pending:t,wallet:s.wallet}),d(P,{children:"Bag"}),d(fe,{inventory:s.state.inventory,onUse:o,pending:n}),f===null?null:d(G,{role:"alert",children:f}),d(P,{children:"Pokédex"}),d(he,{entries:s.dex,pluginId:r})]})}var Ut=V({mount:Ne});export{T as activityOf,Ut as default};
308
+ border: 1px solid var(--rule);
309
+ border-radius: 8px;
310
+ `,te=i.div`
311
+ display: flex;
312
+ align-items: center;
313
+ gap: ${l.sm};
314
+ `,ne=i.span`
315
+ flex: 1;
316
+ overflow-wrap: anywhere;
317
+ `,oe=i.p`
318
+ margin: 0;
319
+ color: var(--ink-dim);
320
+ font-size: 12px;
321
+ line-height: 1.45;
322
+ `,re=i.div`
323
+ display: flex;
324
+ align-items: center;
325
+ justify-content: flex-end;
326
+ gap: ${l.sm};
327
+ margin-top: auto;
328
+ `,qe=i.span`
329
+ display: inline-flex;
330
+ align-items: center;
331
+ justify-content: center;
332
+ flex: none;
333
+ width: 32px;
334
+ height: 32px;
335
+ font-size: 20px;
336
+ line-height: 1;
337
+ `,Je=i.img`
338
+ width: 32px;
339
+ height: 32px;
340
+ image-rendering: pixelated;
341
+ `;import{jsx as Qe}from"react/jsx-runtime";function ie({item:e,emoji:t,pluginId:o}){let[a,u]=ut(!1);return Qe(qe,{"aria-hidden":"true",children:a?t:Qe(Je,{alt:"",onError:()=>u(!0),src:Ie(o,e)})})}import{jsx as A,jsxs as ae}from"react/jsx-runtime";function We({inventory:e,onUse:t,pending:o,pluginId:a}){let u=Object.entries(e).filter(([,s])=>s>0);if(u.length===0)return A(x,{children:"Nothing in the bag."});return A(X,{children:u.map(([s,r])=>{let n=N(s);return ae(ee,{children:[ae(te,{children:[A(ie,{emoji:n.emoji,item:s,pluginId:a}),A(ne,{children:L(s)}),ae(O,{children:["×",r]})]}),A(oe,{children:n.blurb}),A(re,{children:n.consumable?ae(v,{disabled:o,onClick:()=>t(s),type:"button",children:["Use ",L(s)]}):A(x,{children:"held"})})]},s)})})}import{Fragment as dt,useState as Ye}from"react";import{jsx as y,jsxs as C,Fragment as gt}from"react/jsx-runtime";function Ze({entries:e,pluginId:t}){let[o,a]=Ye(null),[u,s]=Ye(null);if(e.length===0)return y(x,{children:"Nothing graduated yet."});let r=o===null?e:e.filter((n)=>n.rarity===o);return C(gt,{children:[y(w,{children:$e.map((n)=>y(v,{"aria-pressed":o===n,onClick:()=>{a(n),s(null)},type:"button",children:n??"all"},n??"all"))}),r.length===0?C(x,{children:["No ",o," graduates yet."]}):y(Me,{children:r.map((n)=>{let p=u===n.id,I=`dex-detail-${n.id}`;return C(dt,{children:[C(Fe,{$open:p,"aria-controls":I,"aria-expanded":p,onClick:()=>s(p?null:n.id),type:"button",children:[y("img",{alt:`${n.rarity}${n.isShiny?" shiny":""} ${n.name??`species ${n.finalId}`}`,src:T(t,n.finalId,n.isShiny),style:{width:"64px",height:"64px",imageRendering:"pixelated"}}),y(Y,{children:R(n.name,n.finalId)}),n.nature===null?null:y(Y,{children:n.nature})]}),p?y(ct,{entry:n,id:I,pluginId:t}):null]},n.id)})})]})}function ct({entry:e,id:t,pluginId:o}){return C(Ue,{id:t,children:[y("img",{alt:P(e.name,e.finalId,e.isShiny),src:T(o,e.finalId,e.isShiny),style:{width:"96px",height:"96px",imageRendering:"pixelated"}}),C(Ge,{children:[y("strong",{children:R(e.name,e.finalId)}),C(w,{children:[y(b,{children:e.rarity}),e.isShiny?y(j,{children:"✦ shiny"}):null,e.nature===null?null:y(b,{children:e.nature})]}),y(He,{children:e.chainOrder.map((a,u)=>C(Ve,{children:[y("img",{alt:P(null,a,!1),src:T(o,a,!1)}),y(Y,{children:a===e.finalId?R(e.name,a):`#${a}`})]},`${a}-${u}`))}),C(x,{children:["caught ",new Date(e.caughtAt).toLocaleDateString()]})]})]})}import{jsx as he,jsxs as mt}from"react/jsx-runtime";function fe({stages:e,stageIndex:t,progress:o,threshold:a,label:u,valueText:s}){let r=o/Math.max(1,a)*100;return he(Oe,{"aria-label":u,"aria-valuemax":a,"aria-valuemin":0,"aria-valuenow":o,"aria-valuetext":s,role:"progressbar",children:Array.from({length:Math.max(1,e)},(n,p)=>mt(Ke,{children:[p<t?he(me,{$pct:100}):null,p===t?he(me,{$pct:r}):null]},p))})}function Xe(e,t,o,a){return`stage ${t+1} of ${e}, ${h(o)} of ${h(a)} tokens`}import{jsx as c,jsxs as g,Fragment as U}from"react/jsx-runtime";function et({view:e,activity:t,pluginId:o,onRelease:a,releasing:u}){let s=e.state;if(s===null)return null;let{active:r}=s,n=r===null?null:r.plannedPath[r.stageIndex]??null;return g(U,{children:[g(w,{children:[n===null?c(q,{"aria-label":"An egg, not yet hatched",role:"img"}):c(z,{alt:P(e.name,n,r?.isShiny===!0),src:T(o,n,r?.isShiny===!0)}),g("div",{children:[c("h3",{children:n===null?"Egg":R(e.name,n)}),g(Le,{children:[r===null?s.eggTier===null?null:g(b,{children:[s.eggTier,"+ guaranteed"]}):g(U,{children:[c(b,{children:r.rarity}),r.isShiny?g(j,{children:[c("span",{"aria-hidden":"true",children:"✦"}),"shiny"]}):null,c(b,{children:r.nature}),r.dittoDisguise===null||r.dittoRevealed?null:c(b,{children:"?"}),r.everstone?c(b,{children:"everstone"}):null,r.soothe?c(b,{children:"soothe bell"}):null]}),c(b,{"aria-label":`Activity: ${t}`,role:"status",children:t})]}),r===null?g(U,{children:[c(fe,{label:"Incubation",progress:e.progress,stageIndex:0,stages:1,threshold:e.nextThreshold,valueText:`${h(e.progress)} of ${h(e.nextThreshold)} tokens incubated`}),g(F,{children:[h(e.progress)," / ",h(e.nextThreshold)," tokens incubated"]})]}):g(U,{children:[c(fe,{label:"Growth to the next evolution",progress:e.progress,stageIndex:r.stageIndex,stages:r.plannedPath.length,threshold:e.nextThreshold,valueText:Xe(r.plannedPath.length,r.stageIndex,e.progress,e.nextThreshold)}),g(F,{children:["Stage ",r.stageIndex+1," of ",r.plannedPath.length]}),r.everstone?g(U,{children:[g(F,{children:["Held at this stage · ",h(e.progress)," banked"]}),c(v,{disabled:u,onClick:a,type:"button",children:"Release"})]}):g(F,{children:[h(e.progress)," / ",h(e.nextThreshold)," to the next stage"]})]})]})]}),g(_e,{children:[g(J,{children:[c(Q,{children:"Earned"}),c(W,{children:h(e.tokensTotal)})]}),g(J,{children:[c(Q,{children:"To spend"}),c(W,{children:h(e.wallet)})]}),g(J,{children:[c(Q,{children:"Graduated"}),c(W,{children:e.dex.length.toLocaleString()})]})]})]})}import{useState as ht}from"react";import{jsx as k,jsxs as se}from"react/jsx-runtime";function tt({keys:e,onPick:t,pluginId:o,rosterFailed:a}){let[u,s]=ht("");return se(M,{children:[k("h2",{children:"Companion"}),k(Ne,{children:"Each API key raises its own Pokémon on the tokens it spends. Pick a key."}),e.length>0?k(je,{children:e.map((r)=>k(ft,{entry:r,onPick:t,pluginId:o},r.apiKeyId))}):k(x,{children:a?"The list of keys could not be loaded. Enter a key id below to reach a companion directly.":"No key has spent a token yet. A companion appears the first time a key serves a request."}),k(ce,{children:"Or by key id"}),k("form",{onSubmit:(r)=>{r.preventDefault();let n=u.trim();if(n!=="")t(n)},children:se(w,{children:[k("input",{"aria-label":"API key id",onChange:(r)=>s(r.target.value),placeholder:"key id",value:u}),k(v,{type:"submit",children:"Show"})]})})]})}function ft({entry:e,onPick:t,pluginId:o}){let a=_(e.speciesId!==null,e.lastCreditAt,Date.now());return se(ze,{onClick:()=>t(e.apiKeyId),type:"button",children:[e.unreadable?k(Be,{"aria-label":"This key's save could not be read",role:"img"}):e.speciesId===null?k(q,{"aria-label":"An egg, not yet hatched",role:"img"}):k(z,{alt:P(e.name,e.speciesId,e.isShiny),src:T(o,e.speciesId,e.isShiny)}),k("strong",{children:e.unreadable?"Save unreadable":e.speciesId===null?"Egg":R(e.name,e.speciesId)}),e.rarity===null?null:k(b,{children:e.rarity}),k(Z,{children:e.apiKeyId}),se(x,{children:[k(O,{children:h(e.tokensTotal)}),e.unreadable?null:` · ${a}`]})]})}import{useId as xt,useState as bt}from"react";import{jsx as G,jsxs as nt,Fragment as kt}from"react/jsx-runtime";var K={shop:!0,bag:!0,dex:!0},ot=(e)=>`plugin:${e}:sections`;function yt(e){try{let t=globalThis.localStorage?.getItem(ot(e));if(t===null||t===void 0)return K;let o=JSON.parse(t);if(typeof o!=="object"||o===null)return K;let a=o;return{shop:typeof a.shop==="boolean"?a.shop:K.shop,bag:typeof a.bag==="boolean"?a.bag:K.bag,dex:typeof a.dex==="boolean"?a.dex:K.dex}}catch{return K}}function rt(e){let[t,o]=bt(()=>yt(e));return{open:t,toggle:(u)=>{o((s)=>{let r={...s,[u]:!s[u]};try{globalThis.localStorage?.setItem(ot(e),JSON.stringify(r))}catch{}return r})}}}function le({title:e,count:t,open:o,onToggle:a,children:u}){let s=xt();return nt(kt,{children:[G(Te,{children:nt(Pe,{"aria-controls":s,"aria-expanded":o,onClick:a,type:"button",children:[G(Ae,{$open:o,children:"▶"}),e,G(Re,{}),G(Ee,{children:t})]})}),o?G("div",{id:s,children:u}):null]})}import{jsx as E,jsxs as pe}from"react/jsx-runtime";function it({offers:e,wallet:t,onBuy:o,pending:a,inventory:u,hasCompanion:s,pluginId:r}){return E(X,{children:e.map((n)=>{let{entry:p,price:I}=n,S=St(p,u),d=p.kind==="egg"&&!s,D=p.kind==="item"?L(p.item):"fresh egg";return pe(ee,{children:[pe(te,{children:[E(ie,{emoji:p.kind==="item"?N(p.item).emoji:ke,item:p.kind==="item"?p.item:"egg",pluginId:r}),E(ne,{children:D}),S?E(x,{children:"owned"}):E(O,{children:h(I)})]}),p.kind==="egg"&&p.tier!==null?pe(b,{children:[p.tier,"+"]}):null,E(oe,{children:p.kind==="item"?N(p.item).blurb:Se(p.tier)}),E(re,{children:pe(v,{disabled:S||d||t<I||a,onClick:()=>o(p),type:"button",children:["Buy ",we(p)]})})]},p.kind==="item"?`item:${p.item}`:`egg:${p.tier??""}`)})})}function St(e,t){if(e.kind!=="item")return!1;return!N(e.item).consumable&&(t[e.item]??0)>0}import{jsx as f,jsxs as be,Fragment as Ct}from"react/jsx-runtime";var It=15000;function wt({pluginId:e,api:t}){let[o,a]=st({at:"start"}),u=vt(),s=at({queryKey:["roster"],queryFn:()=>t.get("keys")}),r=s.data?.keys??[];if(o.at==="start"&&!s.isPending){let m=r.length===1?r[0]:void 0;a(m===void 0?{at:"roster"}:{at:"key",apiKeyId:m.apiKeyId})}let n=o.at==="key"?o.apiKeyId:null,p=at({queryKey:["companion",n],queryFn:()=>t.get(`keys/${n}`),enabled:n!==null,refetchInterval:It}),[I,S]=st(null),d=()=>{u.invalidateQueries({queryKey:["companion",n]}),u.invalidateQueries({queryKey:["roster"]})},D=xe({mutationFn:(m)=>t.post(`keys/${n}/purchase`,m),onMutate:()=>S(null),onError:(m)=>S(m instanceof Error?m.message:"the purchase was refused"),onSuccess:d}),H=xe({mutationFn:(m)=>t.post(`keys/${n}/use`,{item:m}),onMutate:()=>S(null),onError:(m)=>S(m instanceof Error?m.message:"the item could not be used"),onSuccess:d}),V=xe({mutationFn:()=>t.post(`keys/${n}/unpin`),onMutate:()=>S(null),onError:(m)=>S(m instanceof Error?m.message:"the companion could not be released"),onSuccess:d});if(s.isPending)return f(M,{children:"Loading…"});if(n===null)return f(tt,{keys:r,onPick:(m)=>a({at:"key",apiKeyId:m}),pluginId:e,rosterFailed:s.isError});return be(M,{children:[be(De,{children:[f("h2",{children:"Companion"}),f(Z,{children:n}),f(v,{onClick:()=>a({at:"roster"}),type:"button",children:"All keys"})]}),f($t,{buy:D.mutate,buying:D.isPending,keyId:n,onRelease:()=>V.mutate(),onUse:H.mutate,pluginId:e,query:p,refusal:I,releasing:V.isPending,using:H.isPending})]})}function $t({query:e,pluginId:t,buy:o,onUse:a,onRelease:u,buying:s,using:r,releasing:n,refusal:p}){let{open:I,toggle:S}=rt(t);if(e.isPending)return f(x,{children:"Loading…"});if(e.isError||e.data===void 0)return f(x,{children:"No companion for that key yet."});let d=e.data;if(d.state===null)return f(ge,{children:"This key's save could not be read. It has been left untouched rather than replaced — nothing has been lost, but it needs looking at."});let D=_(d.state.active!==null,d.lastCreditAt,Date.now()),H=Object.values(d.state.inventory).filter((V)=>V>0).length;return be(Ct,{children:[f(et,{activity:D,onRelease:u,pluginId:t,releasing:n,view:d}),f(le,{count:lt(d.shop.length,"offer"),onToggle:()=>S("shop"),open:I.shop,title:"Shop",children:f(it,{hasCompanion:d.state.active!==null,inventory:d.state.inventory,offers:d.shop,onBuy:o,pending:s,pluginId:t,wallet:d.wallet})}),f(le,{count:`${H} held`,onToggle:()=>S("bag"),open:I.bag,title:"Bag",children:f(We,{inventory:d.state.inventory,onUse:a,pending:r,pluginId:t})}),p===null?null:f(ge,{role:"alert",children:p}),f(le,{count:lt(d.dex.length,"graduate"),onToggle:()=>S("dex"),open:I.dex,title:"Pokédex",children:f(Ze,{entries:d.dex,pluginId:t})})]})}function lt(e,t){return`${e} ${t}${e===1?"":"s"}`}var jn=ue({mount:wt});export{_ as activityOf,jn as default};