@omnigateway/pokemon 1.0.0 → 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.0",
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.0",
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,16 @@ 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
+ }
374
+ async function cachedSpeciesName(deps, id) {
375
+ if (!isFetchableSpeciesId(id))
376
+ return null;
377
+ const cached = parseCachedDetail(await readJson(deps, speciesPath(id)), id);
378
+ return cached?.names.en ?? null;
379
+ }
351
380
  function parseCachedIndex(raw) {
352
381
  const entries = asArray(raw);
353
382
  if (entries === null)
@@ -421,6 +450,22 @@ async function spriteBytes(deps, id, shiny) {
421
450
  await writeCache(deps, path, bytes);
422
451
  return bytes;
423
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
+ }
424
469
 
425
470
  // src/roll.ts
426
471
  var NATURES = [
@@ -461,23 +506,33 @@ function mulberry32(seed) {
461
506
  };
462
507
  }
463
508
  var COLLECTED_WEIGHT = 0.25;
509
+ var FORM_WEIGHT = 0.6;
464
510
  function roll(input) {
465
511
  const random = mulberry32(input.seed);
466
- const eligible = input.candidates.filter((candidate) => {
512
+ const eligibleWith = (withLure) => input.candidates.filter((candidate) => {
467
513
  if (!hasAnimatedSprite(candidate.id))
468
514
  return false;
469
515
  if (candidate.id === DITTO_SPECIES_ID)
470
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;
471
521
  if (input.guarantee === null)
472
522
  return true;
473
523
  const rarity2 = rarityFromCaptureRate(candidate.captureRate, false, false);
474
524
  return sortRank(rarity2) >= sortRank(input.guarantee);
475
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);
476
530
  if (eligible.length === 0)
477
531
  return null;
478
532
  const weights = eligible.map((candidate) => {
479
533
  const base = Math.max(1, candidate.captureRate);
480
- 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;
481
536
  });
482
537
  const total = weights.reduce((a, b) => a + b, 0);
483
538
  let target = random() * total;
@@ -496,10 +551,16 @@ function roll(input) {
496
551
  const rarity = rarityFromCaptureRate(chosen.captureRate, false, false);
497
552
  const dittoEligible = rarity === "common" && chosen.forms >= 2;
498
553
  const ditto = dittoEligible && random() < 1 / ODDS.dittoDisguise;
499
- return { speciesId: chosen.id, isShiny, nature, ditto };
554
+ return { speciesId: chosen.id, isShiny, nature, ditto, usedLure };
500
555
  }
501
556
 
502
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
+ }
503
564
  function freshState() {
504
565
  return {
505
566
  consumedTotal: 0,
@@ -507,7 +568,11 @@ function freshState() {
507
568
  eggUsage: 0,
508
569
  eggTier: null,
509
570
  pendingHatch: null,
510
- inventory: { rareCandy: 0, mint: 0, shinyCharm: 0 }
571
+ pendingReveal: null,
572
+ lure: false,
573
+ incense: false,
574
+ repel: null,
575
+ inventory: emptyInventory()
511
576
  };
512
577
  }
513
578
  function isRecord(value) {
@@ -528,7 +593,7 @@ function parseState(raw) {
528
593
  }
529
594
  if (!isRecord(parsed))
530
595
  return null;
531
- const inventory = { rareCandy: 0, mint: 0, shinyCharm: 0 };
596
+ const inventory = emptyInventory();
532
597
  const storedInventory = parsed.inventory;
533
598
  if (isRecord(storedInventory)) {
534
599
  for (const kind of ITEM_KINDS) {
@@ -556,7 +621,10 @@ function parseState(raw) {
556
621
  isShiny: storedActive.isShiny === true,
557
622
  nature: nature ?? "hardy",
558
623
  dittoDisguise: typeof storedActive.dittoDisguise === "number" ? storedActive.dittoDisguise : null,
559
- 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))
560
628
  };
561
629
  }
562
630
  const storedPending = parsed.pendingHatch;
@@ -576,13 +644,28 @@ function parseState(raw) {
576
644
  };
577
645
  }
578
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;
579
658
  const eggTier = asRarity(parsed.eggTier);
580
659
  return {
581
- consumedTotal: Math.max(0, asInt(parsed.consumedTotal, 0)),
660
+ consumedTotal: Math.max(0, Math.trunc(storedConsumed)),
582
661
  active,
583
662
  eggUsage: Math.max(0, asInt(parsed.eggUsage, 0)),
584
663
  eggTier: eggTier === null || eggTier === "legendary" ? null : eggTier,
585
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,
586
669
  inventory
587
670
  };
588
671
  }
@@ -600,7 +683,19 @@ function advance(state, tokensTotal) {
600
683
  const events = [];
601
684
  let next = { ...state, consumedTotal: Math.trunc(tokensTotal) };
602
685
  if (gained > 0) {
603
- 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
+ }
604
699
  }
605
700
  for (let step = 0;step < MAX_TRANSITIONS_PER_ADVANCE; step++) {
606
701
  if (next.active === null) {
@@ -618,7 +713,10 @@ function advance(state, tokensTotal) {
618
713
  isShiny: hatch.isShiny,
619
714
  nature: hatch.nature,
620
715
  dittoDisguise: hatch.ditto ? hatch.speciesId : null,
621
- dittoRevealed: false
716
+ dittoRevealed: false,
717
+ everstone: false,
718
+ soothe: false,
719
+ soothedRaw: 0
622
720
  };
623
721
  events.push({
624
722
  kind: "hatched",
@@ -630,10 +728,36 @@ function advance(state, tokensTotal) {
630
728
  continue;
631
729
  }
632
730
  const mon = next.active;
731
+ if (mon.everstone)
732
+ break;
633
733
  const needed = phaseThreshold(mon.rarity, mon.plannedPath.length, mon.stageIndex);
634
734
  if (mon.usedAtStage < needed)
635
735
  break;
636
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
+ }
637
761
  if (mon.stageIndex < mon.plannedPath.length - 1) {
638
762
  events.push({
639
763
  kind: "evolved",
@@ -726,6 +850,18 @@ function readCompanion(storage, apiKeyId) {
726
850
  lastCreditAt: row.last_credit_at
727
851
  };
728
852
  }
853
+ function listCompanions(storage) {
854
+ const rows = storage.all(`SELECT api_key_id, state, tokens_total, tokens_spent, last_credit_at
855
+ FROM {{companion}}
856
+ ORDER BY last_credit_at IS NULL, last_credit_at DESC, tokens_total DESC, api_key_id ASC`);
857
+ return rows.map((row) => ({
858
+ apiKeyId: row.api_key_id,
859
+ state: parseState(row.state),
860
+ tokensTotal: row.tokens_total,
861
+ tokensSpent: row.tokens_spent,
862
+ lastCreditAt: row.last_credit_at
863
+ }));
864
+ }
729
865
  function creditTokens(storage, apiKeyId, tokens, now) {
730
866
  if (tokens <= 0)
731
867
  return;
@@ -814,10 +950,16 @@ function consume(storage, apiKeyId, item, applyToState, now) {
814
950
  return { ok: false, reason: "unreadable" };
815
951
  if ((row.state.inventory[item] ?? 0) <= 0)
816
952
  return { ok: false, reason: "none-held" };
817
- const nextState = applyToState({
818
- ...row.state,
819
- inventory: { ...row.state.inventory, [item]: (row.state.inventory[item] ?? 0) - 1 }
820
- });
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
+ };
821
963
  storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
822
964
  serialiseState(nextState),
823
965
  now,
@@ -835,7 +977,10 @@ function purchase(storage, apiKeyId, entry, applyToState, now) {
835
977
  return { ok: false, reason: "unreadable" };
836
978
  if (wallet(row) < price)
837
979
  return { ok: false, reason: "insufficient" };
838
- 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;
839
984
  storage.run("UPDATE {{companion}} SET state = ?, tokens_spent = tokens_spent + ?, updated_at = ? WHERE api_key_id = ?", [serialiseState(nextState), price, now, apiKeyId]);
840
985
  return {
841
986
  ok: true,
@@ -846,6 +991,9 @@ function purchase(storage, apiKeyId, entry, applyToState, now) {
846
991
 
847
992
  // src/server.ts
848
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;
849
997
  function multiplierFrom(config) {
850
998
  const raw = config.multiplier;
851
999
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
@@ -872,10 +1020,57 @@ var server_default = definePlugin({
872
1020
  const existing = inFlight.get(apiKeyId);
873
1021
  if (existing !== undefined)
874
1022
  return existing;
875
- const started = prefetchHatch(apiKeyId, state).finally(() => inFlight.delete(apiKeyId));
1023
+ const started = prefetchHatch(apiKeyId, state).then(() => prefetchReveal(apiKeyId, state)).finally(() => inFlight.delete(apiKeyId));
876
1024
  inFlight.set(apiKeyId, started);
877
1025
  return started;
878
1026
  };
1027
+ const names = new Map;
1028
+ const nameOf = async (speciesId) => {
1029
+ if (speciesId === null || files === undefined)
1030
+ return null;
1031
+ const known = names.get(speciesId);
1032
+ if (known !== undefined)
1033
+ return known;
1034
+ const found = await cachedSpeciesName({ files }, speciesId);
1035
+ if (found !== null)
1036
+ names.set(speciesId, found);
1037
+ return found;
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
+ };
879
1074
  const settleAndRecord = (apiKeyId) => {
880
1075
  const result = settle(storage, apiKeyId, ctx.now());
881
1076
  if (result === null)
@@ -895,6 +1090,37 @@ var server_default = definePlugin({
895
1090
  ctx.logger.info("companion graduated", { event: "companion.graduated", count: 1 });
896
1091
  }
897
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
+ };
898
1124
  const prefetchHatch = async (apiKeyId, state) => {
899
1125
  if (state.active !== null || state.pendingHatch !== null)
900
1126
  return;
@@ -909,7 +1135,10 @@ var server_default = definePlugin({
909
1135
  seed: hashSeed(`${apiKeyId}:${state.consumedTotal}`),
910
1136
  guarantee: state.eggTier,
911
1137
  hasShinyCharm: hasShinyCharm(state),
912
- collectedFinals: collected
1138
+ collectedFinals: collected,
1139
+ onlyUncollected: state.lure,
1140
+ preferLongLines: state.incense,
1141
+ excludeFinal: state.repel
913
1142
  });
914
1143
  if (rolled === null)
915
1144
  return;
@@ -921,6 +1150,10 @@ var server_default = definePlugin({
921
1150
  const current = readCompanion(storage, apiKeyId);
922
1151
  if (current?.state == null || current.state.pendingHatch !== null)
923
1152
  return;
1153
+ if (current.state.active !== null)
1154
+ return;
1155
+ if (paidRollInputs(current.state) !== paidRollInputs(state))
1156
+ return;
924
1157
  storage.run("UPDATE {{companion}} SET state = ?, updated_at = ? WHERE api_key_id = ?", [
925
1158
  JSON.stringify({
926
1159
  ...current.state,
@@ -931,7 +1164,10 @@ var server_default = definePlugin({
931
1164
  isShiny: rolled.isShiny,
932
1165
  nature: rolled.nature,
933
1166
  ditto: rolled.ditto
934
- }
1167
+ },
1168
+ lure: state.lure && !rolled.usedLure,
1169
+ incense: false,
1170
+ repel: null
935
1171
  }),
936
1172
  ctx.now(),
937
1173
  apiKeyId
@@ -980,10 +1216,33 @@ var server_default = definePlugin({
980
1216
  });
981
1217
  }
982
1218
  const routes = [
1219
+ {
1220
+ method: "GET",
1221
+ path: "/keys",
1222
+ handler: async () => {
1223
+ const rows = listCompanions(storage);
1224
+ const keys = await Promise.all(rows.map(async (row) => {
1225
+ const active = row.state?.active ?? null;
1226
+ const speciesId = active === null ? null : active.plannedPath[active.stageIndex] ?? null;
1227
+ return {
1228
+ apiKeyId: row.apiKeyId,
1229
+ speciesId,
1230
+ name: await nameOf(speciesId),
1231
+ rarity: active?.rarity ?? null,
1232
+ isShiny: active?.isShiny ?? false,
1233
+ tokensTotal: row.tokensTotal,
1234
+ wallet: wallet(row),
1235
+ lastCreditAt: row.lastCreditAt,
1236
+ unreadable: row.state === null
1237
+ };
1238
+ }));
1239
+ return { json: { keys } };
1240
+ }
1241
+ },
983
1242
  {
984
1243
  method: "GET",
985
1244
  path: "/keys/:id",
986
- handler: (request) => {
1245
+ handler: async (request) => {
987
1246
  const apiKeyId = request.params.id ?? "";
988
1247
  settleAndRecord(apiKeyId);
989
1248
  const row = readCompanion(storage, apiKeyId);
@@ -992,13 +1251,22 @@ var server_default = definePlugin({
992
1251
  if (row.state !== null)
993
1252
  prefetchOnce(apiKeyId, row.state).catch(() => {});
994
1253
  const active = row.state?.active ?? null;
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
+ ]);
995
1262
  return {
996
1263
  json: {
997
1264
  state: row.state,
998
1265
  tokensTotal: row.tokensTotal,
999
1266
  wallet: wallet(row),
1000
1267
  lastCreditAt: row.lastCreditAt,
1001
- dex: readDex(storage, apiKeyId),
1268
+ name: stageName,
1269
+ dex: named,
1002
1270
  shop: shopCatalogue(),
1003
1271
  nextThreshold: active === null ? EGG_HATCH_THRESHOLD : phaseThreshold(active.rarity, active.plannedPath.length, active.stageIndex),
1004
1272
  progress: active === null ? row.state?.eggUsage ?? 0 : active.usedAtStage
@@ -1028,6 +1296,27 @@ var server_default = definePlugin({
1028
1296
  };
1029
1297
  }
1030
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
+ },
1031
1320
  {
1032
1321
  method: "POST",
1033
1322
  path: "/keys/:id/use",
@@ -1043,6 +1332,30 @@ var server_default = definePlugin({
1043
1332
  return { json: { ok: true } };
1044
1333
  }
1045
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
+ },
1046
1359
  {
1047
1360
  method: "POST",
1048
1361
  path: "/keys/:id/purchase",
@@ -1061,6 +1374,15 @@ var server_default = definePlugin({
1061
1374
  return { routes };
1062
1375
  }
1063
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
+ }
1064
1386
  function hashSeed(input) {
1065
1387
  let hash = 2166136261;
1066
1388
  for (let i = 0;i < input.length; i++) {
@@ -1081,7 +1403,7 @@ function shopCatalogue() {
1081
1403
  price: freshEggPrice("uncommon")
1082
1404
  },
1083
1405
  { entry: { kind: "egg", tier: "rare" }, price: freshEggPrice("rare") }
1084
- ];
1406
+ ].sort((a, b) => a.price - b.price);
1085
1407
  }
1086
1408
  function parseShopEntry(body) {
1087
1409
  if (typeof body !== "object" || body === null)
@@ -1103,32 +1425,86 @@ function parseShopEntry(body) {
1103
1425
  }
1104
1426
  function applyPurchase(state, entry) {
1105
1427
  if (entry.kind === "egg") {
1106
- 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" };
1107
1436
  }
1108
1437
  return {
1109
- ...state,
1110
- 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
+ }
1111
1442
  };
1112
1443
  }
1444
+ var HELD_ITEMS = [
1445
+ "rareCandy",
1446
+ "mint",
1447
+ "everstone",
1448
+ "lure",
1449
+ "sootheBell",
1450
+ "incense",
1451
+ "repel"
1452
+ ];
1113
1453
  function parseHeldItem(body) {
1114
1454
  if (typeof body !== "object" || body === null)
1115
1455
  return null;
1116
1456
  const item = body.item;
1117
- return item === "rareCandy" || item === "mint" ? item : null;
1457
+ return HELD_ITEMS.includes(item) ? item : null;
1118
1458
  }
1119
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
+ }
1120
1493
  if (item === "mint") {
1121
1494
  if (state.active === null)
1122
- return state;
1495
+ return { refused: "no-companion" };
1123
1496
  const index = NATURES.indexOf(state.active.nature);
1124
1497
  const nature = NATURES[(index + 1) % NATURES.length];
1125
- return { ...state, active: { ...state.active, nature } };
1498
+ return { applied: { ...state, active: { ...state.active, nature } } };
1126
1499
  }
1127
- return state.active === null ? { ...state, eggUsage: state.eggUsage + RARE_CANDY_XP } : {
1128
- ...state,
1129
- 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
+ }
1130
1505
  };
1131
1506
  }
1132
1507
  export {
1508
+ HELD_ITEMS,
1133
1509
  server_default as default
1134
1510
  };
package/ui/index.js CHANGED
@@ -1,68 +1,341 @@
1
- function w(t){return t}import{useMutation as _,useQuery as K,useQueryClient as G}from"@tanstack/react-query";import{useState as f}from"react";import s from"styled-components";import{jsx as n,jsxs as r,Fragment as P}from"react/jsx-runtime";var c=s.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
- border-radius: 6px;
5
- padding: 16px;
4
+ border-radius: 8px;
5
+ padding: ${l.lg};
6
6
  color: var(--ink);
7
- `,b=s.div`
7
+ `,ce=i.h3`
8
+ display: flex;
9
+ align-items: center;
10
+ gap: ${l.md};
11
+ margin: ${l.xl} 0 ${l.md};
12
+ font-size: 11px;
13
+ font-weight: 600;
14
+ letter-spacing: 0.12em;
15
+ text-transform: uppercase;
16
+ color: var(--ink-dim);
17
+
18
+ &::after {
19
+ content: "";
20
+ flex: 1;
21
+ height: 1px;
22
+ background: var(--rule);
23
+ }
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`
8
70
  display: flex;
9
- gap: 16px;
71
+ gap: ${l.md};
10
72
  align-items: center;
11
73
  flex-wrap: wrap;
12
- `,H=s.img`
13
- width: 96px;
14
- height: 96px;
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`
92
+ color: var(--ink-dim);
93
+ margin: ${l.xs} 0 ${l.lg};
94
+ `,ge=i.p`
95
+ color: var(--warn);
96
+ background: var(--warn-wash);
97
+ border-radius: 6px;
98
+ padding: ${l.md};
99
+ margin: ${l.md} 0 0;
100
+ `,b=i.span`
101
+ display: inline-flex;
102
+ align-items: center;
103
+ gap: ${l.xs};
104
+ padding: 2px ${l.sm};
105
+ border: 1px solid var(--rule);
106
+ border-radius: 999px;
107
+ font-size: 11px;
108
+ letter-spacing: 0.08em;
109
+ text-transform: uppercase;
110
+ color: var(--ink-dim);
111
+ white-space: nowrap;
112
+ `,Le=i(w)`
113
+ margin: ${l.xs} 0 ${l.sm};
114
+ `,j=i(b)`
115
+ border-color: var(--rule-strong);
116
+ color: var(--ink);
117
+ font-weight: 600;
118
+ `,z=i.img`
119
+ width: ${B};
120
+ height: ${B};
15
121
  image-rendering: pixelated;
122
+ `,q=i.div`
123
+ box-sizing: border-box;
124
+ width: ${B};
125
+ height: ${B};
126
+ border-radius: 50% 50% 45% 45%;
16
127
  background: var(--panel-sunk);
17
- border-radius: 4px;
18
- `,C=s.div`
19
- background: var(--panel-sunk);
20
- border-radius: 3px;
21
- height: 8px;
22
- overflow: hidden;
23
- min-width: 200px;
24
- `,N=s.div`
25
- background: var(--accent);
26
- height: 100%;
27
- width: ${(t)=>Math.min(100,Math.max(0,t.$pct))}%;
28
- `,a=s.span`
29
- color: var(--ink-dim);
30
- `,J=s.div`
31
- display: grid;
32
- grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
33
- gap: 8px;
34
- margin-top: 12px;
35
- `,x=s.button`
128
+ border: 2px solid var(--rule-strong);
129
+ `,Be=i.div`
130
+ box-sizing: border-box;
131
+ width: ${B};
132
+ height: ${B};
133
+ border-radius: 6px;
134
+ background: var(--warn-wash);
135
+ border: 2px dashed var(--warn);
136
+ `,v=i.button`
36
137
  background: var(--panel-raised);
37
138
  border: 1px solid var(--rule);
38
- border-radius: 4px;
139
+ border-radius: 6px;
39
140
  color: var(--ink);
40
- padding: 6px 10px;
141
+ padding: ${l.sm} ${l.md};
142
+ font: inherit;
41
143
  cursor: pointer;
144
+
145
+ &:hover:not(:disabled) {
146
+ border-color: var(--rule-strong);
147
+ }
148
+ &:focus-visible {
149
+ outline: 2px solid var(--accent);
150
+ outline-offset: 2px;
151
+ }
152
+ &[aria-pressed="true"] {
153
+ background: var(--accent-wash);
154
+ border-color: var(--accent);
155
+ color: var(--ink);
156
+ }
42
157
  &:disabled {
43
158
  color: var(--ink-faint);
44
159
  cursor: not-allowed;
45
160
  }
46
- `,U=s.p`
47
- color: var(--warn);
48
- `,Q=s.figure`
161
+ `,O=i.span`
162
+ font-family: ${de};
163
+ font-variant-numeric: tabular-nums;
164
+ `,Oe=i.div`
165
+ display: flex;
166
+ gap: 3px;
167
+ min-width: 240px;
168
+ max-width: 340px;
169
+ `,Ke=i.div`
170
+ flex: 1;
171
+ height: 10px;
172
+ background: var(--panel-sunk);
173
+ border: 1px solid var(--rule);
174
+ border-radius: 3px;
175
+ overflow: hidden;
176
+ `,me=i.div`
177
+ height: 100%;
178
+ background: var(--accent);
179
+ width: ${(e)=>Math.min(100,Math.max(0,e.$pct))}%;
180
+ transition: width 480ms ease-out;
181
+
182
+ @media (prefers-reduced-motion: reduce) {
183
+ transition: none;
184
+ }
185
+ `,_e=i.dl`
186
+ display: flex;
187
+ flex-wrap: wrap;
188
+ gap: ${l.xl};
189
+ margin: ${l.lg} 0 0;
190
+ padding-top: ${l.lg};
191
+ border-top: 1px solid var(--rule);
192
+ `,J=i.div`
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: 2px;
196
+ `,Q=i.dt`
197
+ font-size: 11px;
198
+ letter-spacing: 0.08em;
199
+ text-transform: uppercase;
200
+ color: var(--ink-faint);
201
+ `,W=i.dd`
202
+ margin: 0;
203
+ font-family: ${de};
204
+ font-variant-numeric: tabular-nums;
205
+ font-size: 18px;
206
+ color: var(--ink);
207
+ `,Me=i.div`
208
+ display: grid;
209
+ grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
210
+ gap: ${l.md};
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`
49
252
  margin: 0;
50
253
  display: flex;
51
254
  flex-direction: column;
52
255
  align-items: center;
53
256
  gap: 2px;
54
- `,j=s.figcaption`
257
+
258
+ img {
259
+ width: 48px;
260
+ height: 48px;
261
+ image-rendering: pixelated;
262
+ }
263
+ `,Y=i.figcaption`
55
264
  color: var(--ink-dim);
56
265
  font-size: 11px;
57
266
  text-align: center;
58
- `,W=s.div`
267
+ overflow-wrap: anywhere;
268
+ `,je=i.div`
269
+ display: grid;
270
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
271
+ gap: ${l.md};
272
+ `,ze=i.button`
59
273
  display: flex;
274
+ flex-direction: column;
60
275
  align-items: center;
61
- gap: 8px;
62
- `,z=s.div`
63
- width: 96px;
64
- height: 96px;
65
- border-radius: 50% 50% 45% 45%;
276
+ gap: ${l.sm};
277
+ padding: ${l.md};
66
278
  background: var(--panel-raised);
67
- border: 2px solid var(--rule-strong);
68
- `;function B(t,l,d){return`/api/plugins/${t}/sprite/${l}${d?"?shiny=1":""}`}function p(t){if(t>=1e9)return`${(t/1e9).toFixed(2)}B`;if(t>=1e6)return`${(t/1e6).toFixed(1)}M`;return t.toLocaleString()}function S(t){return t.replace(/([A-Z])/g," $1").toLowerCase()}function M(t){if(t.kind==="item")return S(t.item);return t.tier===null?"fresh egg":`fresh egg (${t.tier}+)`}var Y=["rareCandy","mint"],Z=[null,"common","uncommon","rare","legendary"],L=60000,O=60*L;function X(t,l,d){if(!t)return"egg";if(l===null)return"sleep";let g=d-l;if(g<5*L)return"working";if(g<O)return"idle";if(g<8*O)return"tired";return"sleep"}function ee({pluginId:t,api:l}){let[d,g]=f(""),[u,V]=f(""),[m,F]=f(null),A=G(),k=K({queryKey:["companion",u],queryFn:()=>l.get(`keys/${u}`),enabled:u!==""}),[T,h]=f(null),E=_({mutationFn:(e)=>l.post(`keys/${u}/purchase`,e),onMutate:()=>h(null),onError:(e)=>h(e instanceof Error?e.message:"the purchase was refused"),onSuccess:()=>A.invalidateQueries({queryKey:["companion",u]})}),v=_({mutationFn:(e)=>l.post(`keys/${u}/use`,{item:e}),onMutate:()=>h(null),onError:(e)=>h(e instanceof Error?e.message:"the item could not be used"),onSuccess:()=>A.invalidateQueries({queryKey:["companion",u]})});if(u==="")return r(c,{children:[n("h2",{children:"Companion"}),n("p",{children:n(a,{children:"Each API key raises its own Pokémon. Enter a key id to see it."})}),r("form",{onSubmit:(e)=>{e.preventDefault(),V(d.trim())},children:[n("input",{"aria-label":"API key id",onChange:(e)=>g(e.target.value),placeholder:"key id",value:d}),n(x,{type:"submit",children:"Show"})]})]});if(k.isPending)return n(c,{children:"Loading…"});if(k.isError)return n(c,{children:"No companion for that key yet."});let i=k.data;if(i.state===null)return r(c,{children:[n("h2",{children:"Companion"}),n(U,{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{active:o}=i.state,y=o===null?null:o.plannedPath[o.stageIndex],R=X(o!==null,i.lastCreditAt,Date.now()),D=Object.entries(i.state.inventory).filter(([,e])=>e>0),I=m===null?i.dex:i.dex.filter((e)=>e.rarity===m);return r(c,{children:[n("h2",{children:"Companion"}),r(b,{children:[y===void 0||y===null?n(z,{"aria-label":"An egg, not yet hatched",role:"img"}):n(H,{alt:`Species ${y}${o?.isShiny===!0?", shiny":""}`,src:B(t,y,o?.isShiny===!0)}),r("div",{children:[o===null?r(P,{children:[r("div",{children:["Egg",i.state.eggTier===null?"":` (${i.state.eggTier}+ guaranteed)`]}),r(a,{children:[p(i.progress)," / ",p(i.nextThreshold)," tokens incubated"]}),n(C,{"aria-label":"Incubation",children:n(N,{$pct:i.progress/Math.max(1,i.nextThreshold)*100})})]}):r(P,{children:[r("div",{children:["Stage ",o.stageIndex+1," of ",o.plannedPath.length," · ",o.rarity,o.isShiny?" · shiny":"",o.dittoDisguise===null?"":" · ?"]}),n(a,{children:o.nature}),n(C,{"aria-label":"Growth to the next evolution",children:n(N,{$pct:i.progress/Math.max(1,i.nextThreshold)*100})}),r(a,{children:[p(i.progress)," / ",p(i.nextThreshold)," to the next stage"]})]}),n("div",{"aria-label":`Activity: ${R}`,role:"status",children:R})]})]}),n("p",{children:r(a,{children:[p(i.tokensTotal)," tokens earned · ",p(i.wallet)," to spend"]})}),n("h3",{children:"Shop"}),n(b,{children:i.shop.map((e)=>r(x,{disabled:i.wallet<e.price||E.isPending,onClick:()=>E.mutate(e.entry),type:"button",children:[M(e.entry)," · ",p(e.price)]},`${e.entry.kind}:${M(e.entry)}`))}),n("h3",{children:"Bag"}),D.length===0?n(a,{children:"Nothing in the bag."}):n(b,{children:D.map(([e,q])=>r(W,{children:[r("span",{children:[S(e)," · ",q]}),Y.includes(e)?r(x,{disabled:v.isPending,onClick:()=>v.mutate(e),type:"button",children:["Use ",S(e)]}):n(a,{children:"held"})]},e))}),T===null?null:n(U,{role:"alert",children:T}),n("h3",{children:"Pokédex"}),i.dex.length===0?n(a,{children:"Nothing graduated yet."}):r(P,{children:[n(b,{children:Z.map((e)=>n(x,{"aria-pressed":m===e,onClick:()=>F(e),type:"button",children:e??"all"},e??"all"))}),I.length===0?r(a,{children:["No ",m," graduates yet."]}):n(J,{children:I.map((e)=>r(Q,{children:[n("img",{alt:`${e.rarity}${e.isShiny?" shiny":""} species ${e.finalId}`,src:B(t,e.finalId,e.isShiny),style:{width:"64px",height:"64px",imageRendering:"pixelated"}}),e.nature===null?null:n(j,{children:e.nature})]},e.id))})]})]})}var ke=w({mount:ee});export{X as activityOf,ke as default};
279
+ border: 1px solid var(--rule);
280
+ border-radius: 8px;
281
+ color: var(--ink);
282
+ font: inherit;
283
+ cursor: pointer;
284
+ text-align: center;
285
+
286
+ &:hover {
287
+ border-color: var(--rule-strong);
288
+ }
289
+ &:focus-visible {
290
+ outline: 2px solid var(--accent);
291
+ outline-offset: 2px;
292
+ }
293
+ `,Z=i.span`
294
+ font-family: ${de};
295
+ font-size: 12px;
296
+ color: var(--ink-dim);
297
+ overflow-wrap: anywhere;
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`
303
+ display: flex;
304
+ flex-direction: column;
305
+ gap: ${l.sm};
306
+ padding: ${l.md};
307
+ background: var(--panel-sunk);
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};