claude-petpet 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2026 Rayhan Noufal Arayilakath
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # claude-petpet
2
+
3
+ Find UUIDs that produce specific [Claude Code](https://docs.anthropic.com/en/docs/claude-code) buddy companions.
4
+
5
+ Claude Code assigns each user a procedurally generated buddy based on their
6
+ account UUID. This tool brute-force searches random UUIDs to find ones that
7
+ produce a buddy matching your desired species, rarity, stats, and cosmetics.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ bun install
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ Interactive mode (recommended):
18
+
19
+ ```bash
20
+ bun run start
21
+ # or
22
+ bun src/cli.ts
23
+ ```
24
+
25
+ The CLI walks you through selecting filters (species, rarity, eyes, hat, shiny,
26
+ peak/dump stats, minimum total) then searches for matching UUIDs.
27
+
28
+ ### As a library
29
+
30
+ ```ts
31
+ import { rollFrom, search } from "claude-petpet";
32
+
33
+ // Roll a specific UUID
34
+ const buddy = rollFrom("your-uuid-here");
35
+ console.log(buddy);
36
+
37
+ // Search with filters
38
+ const results = search({
39
+ species: "axolotl",
40
+ rarity: "legendary",
41
+ limit: 3,
42
+ max: 10_000_000,
43
+ });
44
+ ```
45
+
46
+ ### Applying a result
47
+
48
+ Once you find a UUID you like:
49
+
50
+ 1. Open `~/.claude/.config.json`
51
+ 2. Set `oauthAccount.accountUuid` to the UUID from the search results
52
+ 3. Restart Claude Code
53
+
54
+ > **Note:** Re-authenticating will overwrite the UUID back to your real one.
55
+
56
+ ## How it works
57
+
58
+ Claude Code's buddy system seeds a Mulberry32 PRNG with a hash of
59
+ `userId + salt`, then rolls rarity, species, eyes, hat, shiny status, and stat
60
+ distribution from that deterministic sequence. This tool replicates that
61
+ algorithm to predict the buddy for any given UUID without running Claude Code.
62
+
63
+ ## Legal notice
64
+
65
+ The companion generation algorithm in this project was derived from source maps
66
+ that were inadvertently published alongside Claude Code's publicly distributed
67
+ client-side JavaScript. No access controls, obfuscation, or technological
68
+ protection measures were circumvented — the source maps were openly served to
69
+ end users via NPM. No proprietary source code was copied verbatim; the algorithm
70
+ was reimplemented from the observed logic.
71
+
72
+ Reading publicly served files does not constitute unauthorized access, and
73
+ reimplementing a functional algorithm from publicly available materials is
74
+ well-established as lawful under:
75
+
76
+ - **No DMCA §1201 issue** — no technological protection measure was bypassed;
77
+ the source maps were publicly accessible without authentication
78
+ - **Fair use doctrine** — functional algorithms are not copyrightable expression
79
+ (see _Oracle v. Google_, 593 U.S. 1 (2021))
80
+ - **DMCA §1201(f)** — reverse engineering for interoperability is independently
81
+ permitted even when protection measures are present (not applicable here)
82
+ - **EU Software Directive (2009/24/EC) Art. 6** — permits analysis for
83
+ interoperability
84
+
85
+ This project is not affiliated with or endorsed by Anthropic, PBC. "Claude" and
86
+ "Claude Code" are trademarks of Anthropic.
87
+
88
+ ## License
89
+
90
+ [MIT](LICENSE) — Copyright (c) 2026 Rayhan Noufal Arayilakath
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "claude-petpet",
3
+ "version": "1.0.0",
4
+ "description": "Find UUIDs that produce specific Claude Code buddy companions",
5
+ "keywords": [
6
+ "buddy",
7
+ "bun",
8
+ "claude",
9
+ "claude-code",
10
+ "companion",
11
+ "petpet"
12
+ ],
13
+ "homepage": "https://github.com/rayhanadev/claude-petpet#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/rayhanadev/claude-petpet/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Rayhan Noufal Arayilakath",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/rayhanadev/claude-petpet.git"
22
+ },
23
+ "bin": {
24
+ "claude-petpet": "src/cli.ts"
25
+ },
26
+ "files": [
27
+ "src",
28
+ "LICENSE",
29
+ "README.md"
30
+ ],
31
+ "type": "module",
32
+ "module": "src/index.ts",
33
+ "exports": {
34
+ ".": "./src/index.ts"
35
+ },
36
+ "scripts": {
37
+ "start": "bun src/cli.ts",
38
+ "lint": "oxlint --type-aware",
39
+ "format": "oxfmt",
40
+ "typecheck": "tsc --noEmit"
41
+ },
42
+ "dependencies": {
43
+ "@clack/prompts": "^1.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/bun": "latest",
47
+ "oxfmt": "^0.43.0",
48
+ "oxlint": "^1.58.0",
49
+ "oxlint-tsgolint": "^0.18.1"
50
+ },
51
+ "peerDependencies": {
52
+ "typescript": "^6"
53
+ },
54
+ "engines": {
55
+ "bun": ">=1.2"
56
+ }
57
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env bun
2
+ // CLI for finding UUIDs that produce specific Claude Code buddy companions.
3
+
4
+ import * as p from "@clack/prompts";
5
+
6
+ import { RARITIES, SPECIES, EYES, HATS, STAT_NAMES } from "./enums.ts";
7
+ import { STARS, YIELD_EVERY } from "./consts.ts";
8
+ import { rollFrom, matchesFilters } from "./helpers.ts";
9
+ import { renderSprite } from "./sprites.ts";
10
+ import { c } from "./color.ts";
11
+ import type { Rarity, StatName, SearchFilters, SearchResult } from "./types.ts";
12
+
13
+ const NONE = "__none__";
14
+
15
+ async function promptFilters() {
16
+ return p.group(
17
+ {
18
+ species: () =>
19
+ p.select({
20
+ message: "Species?",
21
+ options: [
22
+ { value: NONE, label: "Any", hint: "no filter" },
23
+ ...SPECIES.map((s) => ({ value: s, label: s })),
24
+ ],
25
+ initialValue: NONE,
26
+ }),
27
+
28
+ rarity: () =>
29
+ p.select({
30
+ message: "Rarity?",
31
+ options: [
32
+ { value: NONE, label: "Any", hint: "no filter" },
33
+ ...RARITIES.map((r) => ({ value: r, label: `${STARS[r]} ${r}` })),
34
+ ],
35
+ initialValue: NONE,
36
+ }),
37
+
38
+ eye: () =>
39
+ p.select({
40
+ message: "Eye style?",
41
+ options: [
42
+ { value: NONE, label: "Any", hint: "no filter" },
43
+ ...EYES.map((e) => ({ value: e, label: e })),
44
+ ],
45
+ initialValue: NONE,
46
+ }),
47
+
48
+ hat: () =>
49
+ p.select({
50
+ message: "Hat?",
51
+ options: [
52
+ { value: NONE, label: "Any", hint: "no filter" },
53
+ ...HATS.map((h) => ({ value: h, label: h })),
54
+ ],
55
+ initialValue: NONE,
56
+ }),
57
+
58
+ shiny: () =>
59
+ p.confirm({
60
+ message: "Must be shiny?",
61
+ initialValue: false,
62
+ }),
63
+
64
+ peak: () =>
65
+ p.select({
66
+ message: "Peak stat?",
67
+ options: [
68
+ { value: NONE, label: "Any", hint: "no filter" },
69
+ ...STAT_NAMES.map((s) => ({ value: s, label: s })),
70
+ ],
71
+ initialValue: NONE,
72
+ }),
73
+
74
+ dump: ({ results }) =>
75
+ p.select({
76
+ message: "Dump stat?",
77
+ options: [
78
+ { value: NONE, label: "Any", hint: "no filter" },
79
+ ...STAT_NAMES.filter(
80
+ (s) => s !== results.peak || (results.peak as string) === NONE,
81
+ ).map((s) => ({
82
+ value: s,
83
+ label: s,
84
+ })),
85
+ ],
86
+ initialValue: NONE,
87
+ }),
88
+
89
+ minTotal: () =>
90
+ p.text({
91
+ message: "Minimum total stats? (0-500)",
92
+ placeholder: "none",
93
+ validate: (v) => {
94
+ if (!v) return;
95
+ const n = parseInt(v, 10);
96
+ if (isNaN(n) || n < 0 || n > 500) return "Must be a number between 0 and 500";
97
+ },
98
+ }),
99
+
100
+ limit: () =>
101
+ p.text({
102
+ message: "How many results?",
103
+ initialValue: "5",
104
+ validate: (v) => {
105
+ if (!v) return "Required";
106
+ const n = parseInt(v, 10);
107
+ if (isNaN(n) || n < 1 || n > 100) return "Must be a number between 1 and 100";
108
+ },
109
+ }),
110
+
111
+ max: () =>
112
+ p.text({
113
+ message: "Max seeds to search?",
114
+ initialValue: "100000000",
115
+ validate: (v) => {
116
+ if (!v) return "Required";
117
+ const n = parseInt(v, 10);
118
+ if (isNaN(n) || n < 1) return "Must be a positive number";
119
+ },
120
+ }),
121
+ },
122
+ {
123
+ onCancel: () => {
124
+ p.cancel("Search cancelled.");
125
+ process.exit(0);
126
+ },
127
+ },
128
+ );
129
+ }
130
+
131
+ function parseFilters(raw: Awaited<ReturnType<typeof promptFilters>>): SearchFilters {
132
+ return {
133
+ species: raw.species !== NONE ? raw.species : undefined,
134
+ rarity: (raw.rarity !== NONE ? raw.rarity : undefined) as Rarity | undefined,
135
+ eye: raw.eye !== NONE ? raw.eye : undefined,
136
+ hat: raw.hat !== NONE ? raw.hat : undefined,
137
+ shiny: raw.shiny || undefined,
138
+ peak: (raw.peak !== NONE ? raw.peak : undefined) as StatName | undefined,
139
+ dump: (raw.dump !== NONE ? raw.dump : undefined) as StatName | undefined,
140
+ minTotal: raw.minTotal ? parseInt(raw.minTotal, 10) : undefined,
141
+ limit: parseInt(raw.limit, 10),
142
+ max: parseInt(raw.max, 10),
143
+ };
144
+ }
145
+
146
+ function formatSummary(filters: SearchFilters): string {
147
+ const parts: string[] = [];
148
+ if (filters.species) parts.push(`species=${filters.species}`);
149
+ if (filters.rarity) parts.push(`rarity=${filters.rarity}`);
150
+ if (filters.eye) parts.push(`eye=${filters.eye}`);
151
+ if (filters.hat) parts.push(`hat=${filters.hat}`);
152
+ if (filters.shiny) parts.push("shiny=true");
153
+ if (filters.peak) parts.push(`peak=${filters.peak}`);
154
+ if (filters.dump) parts.push(`dump=${filters.dump}`);
155
+ if (filters.minTotal) parts.push(`min-total=${filters.minTotal}`);
156
+ return parts.join(", ") || "(any)";
157
+ }
158
+
159
+ async function searchWithProgress(
160
+ filters: SearchFilters,
161
+ ): Promise<{ results: SearchResult[]; searched: number }> {
162
+ const s = p.spinner({ indicator: "timer" });
163
+ s.start("Searching...");
164
+
165
+ const results: SearchResult[] = [];
166
+ let searched = 0;
167
+
168
+ for (let i = 0; i < filters.max; i++) {
169
+ if (i % YIELD_EVERY === 0 && i > 0) {
170
+ s.message(`Found ${results.length} match(es), ${i.toLocaleString()} searched`);
171
+ await Bun.sleep(0);
172
+ }
173
+
174
+ const uuid = crypto.randomUUID();
175
+ const roll = rollFrom(uuid);
176
+
177
+ if (!matchesFilters(roll, filters)) continue;
178
+
179
+ results.push({ ...roll, uuid });
180
+ results.sort((a, b) => b.total - a.total);
181
+ if (results.length > filters.limit) results.length = filters.limit;
182
+
183
+ searched = i + 1;
184
+ if (results.length >= filters.limit) break;
185
+ }
186
+
187
+ searched = searched || filters.max;
188
+ s.stop(`Searched ${searched.toLocaleString()} seeds`);
189
+
190
+ return { results, searched };
191
+ }
192
+
193
+ function padVisual(s: string, width: number): string {
194
+ return s + " ".repeat(Math.max(0, width - Bun.stringWidth(s)));
195
+ }
196
+
197
+ function printResult(r: SearchResult) {
198
+ const rarityColor =
199
+ r.rarity === "legendary"
200
+ ? c.yellow
201
+ : r.rarity === "epic"
202
+ ? c.magenta
203
+ : r.rarity === "rare"
204
+ ? c.blue
205
+ : r.rarity === "uncommon"
206
+ ? c.green
207
+ : c.dim;
208
+
209
+ const shinyColor = r.shiny ? c.yellow : (s: string) => s;
210
+
211
+ const header = [
212
+ rarityColor(STARS[r.rarity]),
213
+ rarityColor(r.rarity.toUpperCase()),
214
+ c.bold(r.species),
215
+ r.eye,
216
+ r.hat !== "none" ? c.cyan(`[${r.hat}]`) : "",
217
+ r.shiny ? c.yellow("✨SHINY") : "",
218
+ ]
219
+ .filter(Boolean)
220
+ .join(" ");
221
+
222
+ const statLine = STAT_NAMES.map((s) => {
223
+ const tag = s === r.peak ? c.green("↑") : s === r.dump ? c.red("↓") : " ";
224
+ return `${c.dim(s)}: ${String(r.stats[s]).padStart(3)}${tag}`;
225
+ }).join(" ");
226
+
227
+ const infoLines = [
228
+ header,
229
+ statLine,
230
+ `${c.dim("Total:")} ${r.total}/500`,
231
+ `${c.dim("UUID:")} ${c.cyan(r.uuid)}`,
232
+ ];
233
+
234
+ // Render sprite and colorize it
235
+ const sprite = renderSprite(r).map((line) => shinyColor(line));
236
+ const spriteWidth = 14;
237
+
238
+ // Pad both sides to equal height
239
+ const height = Math.max(sprite.length, infoLines.length);
240
+ while (sprite.length < height) sprite.push(" ".repeat(12));
241
+ while (infoLines.length < height) infoLines.push("");
242
+
243
+ // Combine sprite + info side by side
244
+ const combined = sprite
245
+ .map((spriteLine, i) => {
246
+ const paddedSprite = padVisual(spriteLine, spriteWidth);
247
+ return ` ${paddedSprite}${infoLines[i]}`;
248
+ })
249
+ .join("\n");
250
+
251
+ console.log();
252
+ console.log(combined);
253
+ }
254
+
255
+ async function main() {
256
+ p.intro(c.bgCyanBlack(" claude-petpet "));
257
+
258
+ const raw = await promptFilters();
259
+ const filters = parseFilters(raw);
260
+
261
+ p.log.info(
262
+ `Searching for: ${c.cyan(formatSummary(filters))} ${c.dim(`(top ${filters.limit}, up to ${filters.max.toLocaleString()} seeds)`)}`,
263
+ );
264
+
265
+ const { results } = await searchWithProgress(filters);
266
+
267
+ if (results.length === 0) {
268
+ p.log.warn("No matches found. Try relaxing filters or increasing max seeds.");
269
+ p.outro("Done.");
270
+ return;
271
+ }
272
+
273
+ p.log.success(`Found ${c.green(String(results.length))} match${results.length > 1 ? "es" : ""}!`);
274
+
275
+ for (const r of results) {
276
+ printResult(r);
277
+ }
278
+
279
+ p.log.info(
280
+ "To use: edit ~/.claude/.config.json → set oauthAccount.accountUuid to the UUID above.",
281
+ );
282
+ p.log.warn("Re-authenticating will overwrite it back to your real UUID.");
283
+ p.outro(c.green("Happy hunting!"));
284
+ }
285
+
286
+ void main();
package/src/color.ts ADDED
@@ -0,0 +1,31 @@
1
+ const RESET = "\x1b[0m";
2
+ const BOLD = "\x1b[1m";
3
+ const DIM = "\x1b[2m";
4
+
5
+ function fg(cssColor: string) {
6
+ return (text: string) => {
7
+ const ansi = Bun.color(cssColor, "ansi");
8
+ return ansi ? `${ansi}${text}${RESET}` : text;
9
+ };
10
+ }
11
+
12
+ function bgFg(bgCss: string, fgCss: string) {
13
+ return (text: string) => {
14
+ const bgAnsi = Bun.color(bgCss, "ansi")?.replace("[38;", "[48;");
15
+ const fgAnsi = Bun.color(fgCss, "ansi");
16
+ if (!bgAnsi || !fgAnsi) return text;
17
+ return `${bgAnsi}${fgAnsi}${text}${RESET}`;
18
+ };
19
+ }
20
+
21
+ export const c = {
22
+ bold: (text: string) => `${BOLD}${text}${RESET}`,
23
+ dim: (text: string) => `${DIM}${text}${RESET}`,
24
+ red: fg("red"),
25
+ green: fg("lime"),
26
+ blue: fg("dodgerblue"),
27
+ yellow: fg("yellow"),
28
+ magenta: fg("magenta"),
29
+ cyan: fg("cyan"),
30
+ bgCyanBlack: bgFg("cyan", "black"),
31
+ };
package/src/consts.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { Rarity } from "./types.ts";
2
+
3
+ export const SALT = "friend-2026-401";
4
+
5
+ export const RARITY_WEIGHTS: Record<Rarity, number> = {
6
+ common: 60,
7
+ uncommon: 25,
8
+ rare: 10,
9
+ epic: 4,
10
+ legendary: 1,
11
+ };
12
+
13
+ export const RARITY_FLOOR: Record<Rarity, number> = {
14
+ common: 5,
15
+ uncommon: 15,
16
+ rare: 25,
17
+ epic: 35,
18
+ legendary: 50,
19
+ };
20
+
21
+ export const STARS: Record<Rarity, string> = {
22
+ common: "★",
23
+ uncommon: "★★",
24
+ rare: "★★★",
25
+ epic: "★★★★",
26
+ legendary: "★★★★★",
27
+ };
28
+
29
+ export const YIELD_EVERY = 50_000;
package/src/enums.ts ADDED
@@ -0,0 +1,37 @@
1
+ export const RARITIES = ["common", "uncommon", "rare", "epic", "legendary"] as const;
2
+
3
+ export const SPECIES = [
4
+ "duck",
5
+ "goose",
6
+ "blob",
7
+ "cat",
8
+ "dragon",
9
+ "octopus",
10
+ "owl",
11
+ "penguin",
12
+ "turtle",
13
+ "snail",
14
+ "ghost",
15
+ "axolotl",
16
+ "capybara",
17
+ "cactus",
18
+ "robot",
19
+ "rabbit",
20
+ "mushroom",
21
+ "chonk",
22
+ ] as const;
23
+
24
+ export const EYES = ["·", "✦", "×", "◉", "@", "°"] as const;
25
+
26
+ export const HATS = [
27
+ "none",
28
+ "crown",
29
+ "tophat",
30
+ "propeller",
31
+ "halo",
32
+ "wizard",
33
+ "beanie",
34
+ "tinyduck",
35
+ ] as const;
36
+
37
+ export const STAT_NAMES = ["DEBUGGING", "PATIENCE", "CHAOS", "WISDOM", "SNARK"] as const;
package/src/helpers.ts ADDED
@@ -0,0 +1,104 @@
1
+ import { RARITIES, SPECIES, HATS, EYES, STAT_NAMES } from "./enums.ts";
2
+ import { SALT, RARITY_WEIGHTS, RARITY_FLOOR } from "./consts.ts";
3
+ import type { Rarity, StatName, Roll, SearchFilters, SearchResult } from "./types.ts";
4
+
5
+ // Mulberry32 PRNG — deterministic 32-bit PRNG matching the companion
6
+ // generation algorithm used by Claude Code's buddy system.
7
+ export function mulberry32(seed: number): () => number {
8
+ let a = seed >>> 0;
9
+ return function () {
10
+ a |= 0;
11
+ a = (a + 0x6d2b79f5) | 0;
12
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
13
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
14
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
15
+ };
16
+ }
17
+
18
+ export function hashString(s: string): number {
19
+ if (typeof Bun !== "undefined") {
20
+ return Number(BigInt(Bun.hash(s)) & 0xffffffffn);
21
+ }
22
+ // FNV-1a fallback for non-Bun runtimes
23
+ let h = 2166136261;
24
+ for (let i = 0; i < s.length; i++) {
25
+ h ^= s.charCodeAt(i);
26
+ h = Math.imul(h, 16777619);
27
+ }
28
+ return h >>> 0;
29
+ }
30
+
31
+ export function pick<T>(rng: () => number, arr: readonly T[]): T {
32
+ return arr[Math.floor(rng() * arr.length)]!;
33
+ }
34
+
35
+ export function rollRarity(rng: () => number): Rarity {
36
+ const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0);
37
+ let roll = rng() * total;
38
+ for (const rarity of RARITIES) {
39
+ roll -= RARITY_WEIGHTS[rarity];
40
+ if (roll < 0) return rarity;
41
+ }
42
+ return "common";
43
+ }
44
+
45
+ export function rollStats(rng: () => number, rarity: Rarity) {
46
+ const floor = RARITY_FLOOR[rarity];
47
+ const peak = pick(rng, STAT_NAMES);
48
+ let dump = pick(rng, STAT_NAMES);
49
+ while (dump === peak) dump = pick(rng, STAT_NAMES);
50
+
51
+ const stats = {} as Record<StatName, number>;
52
+ for (const name of STAT_NAMES) {
53
+ if (name === peak) {
54
+ stats[name] = Math.min(100, floor + 50 + Math.floor(rng() * 30));
55
+ } else if (name === dump) {
56
+ stats[name] = Math.max(1, floor - 10 + Math.floor(rng() * 15));
57
+ } else {
58
+ stats[name] = floor + Math.floor(rng() * 40);
59
+ }
60
+ }
61
+ return { stats, peak, dump };
62
+ }
63
+
64
+ export function rollFrom(userId: string): Roll {
65
+ const rng = mulberry32(hashString(userId + SALT));
66
+ const rarity = rollRarity(rng);
67
+ const species = pick(rng, SPECIES);
68
+ const eye = pick(rng, EYES);
69
+ const hat = rarity === "common" ? "none" : pick(rng, HATS);
70
+ const shiny = rng() < 0.01;
71
+ const { stats, peak, dump } = rollStats(rng, rarity);
72
+ const total = Object.values(stats).reduce((a, b) => a + b, 0);
73
+ return { rarity, species, eye, hat, shiny, stats, peak, dump, total };
74
+ }
75
+
76
+ export function matchesFilters(roll: Roll, filters: SearchFilters): boolean {
77
+ if (filters.species && roll.species !== filters.species) return false;
78
+ if (filters.rarity && roll.rarity !== filters.rarity) return false;
79
+ if (filters.eye && roll.eye !== filters.eye) return false;
80
+ if (filters.hat && roll.hat !== filters.hat) return false;
81
+ if (filters.shiny && !roll.shiny) return false;
82
+ if (filters.peak && roll.peak !== filters.peak) return false;
83
+ if (filters.dump && roll.dump !== filters.dump) return false;
84
+ if (filters.minTotal && roll.total < filters.minTotal) return false;
85
+ return true;
86
+ }
87
+
88
+ export function search(filters: SearchFilters): SearchResult[] {
89
+ const results: SearchResult[] = [];
90
+
91
+ for (let i = 0; i < filters.max; i++) {
92
+ const uuid = crypto.randomUUID();
93
+ const roll = rollFrom(uuid);
94
+
95
+ if (!matchesFilters(roll, filters)) continue;
96
+
97
+ results.push({ ...roll, uuid });
98
+ results.sort((a, b) => b.total - a.total);
99
+ if (results.length > filters.limit) results.length = filters.limit;
100
+ if (results.length >= filters.limit) break;
101
+ }
102
+
103
+ return results;
104
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { RARITIES, SPECIES, EYES, HATS, STAT_NAMES } from "./enums.ts";
2
+ export { SALT, RARITY_WEIGHTS, RARITY_FLOOR, STARS } from "./consts.ts";
3
+ export {
4
+ mulberry32,
5
+ hashString,
6
+ pick,
7
+ rollRarity,
8
+ rollStats,
9
+ rollFrom,
10
+ matchesFilters,
11
+ search,
12
+ } from "./helpers.ts";
13
+ export { renderSprite } from "./sprites.ts";
14
+ export type { Rarity, StatName, Roll, SearchFilters, SearchResult } from "./types.ts";
package/src/sprites.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { Roll } from "./types.ts";
2
+
3
+ // Sprite bodies — frame 0 only (static display).
4
+ // Each sprite is 5 lines tall, 12 wide (after {E} → 1-char substitution).
5
+ // Line 0 is the hat slot — blank unless a hat is applied.
6
+ const BODIES: Record<string, string[]> = {
7
+ duck: [" ", " __ ", " <({E} )___ ", " ( ._> ", " `--´ "],
8
+ goose: [" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
9
+ blob: [" ", " .----. ", " ( {E} {E} ) ", " ( ) ", " `----´ "],
10
+ cat: [" ", " /\\_/\\ ", " ( {E} {E}) ", " ( ω ) ", ' (")_(") '],
11
+ dragon: [" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-´ "],
12
+ octopus: [" ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
13
+ owl: [" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " `----´ "],
14
+ penguin: [" ", " .---. ", " ({E}>{E}) ", " /( )\\ ", " `---´ "],
15
+ turtle: [" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
16
+ snail: [" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--´ ", " ~~~~~~~ "],
17
+ ghost: [" ", " .----. ", " / {E} {E} \\ ", " | | ", " ~`~``~`~ "],
18
+ axolotl: [" ", "}~(______)~{", "}~({E} .. {E})~{", " ( .--. ) ", " (_/ \\_) "],
19
+ capybara: [" ", " n______n ", " ( {E} {E} ) ", " ( oo ) ", " `------´ "],
20
+ cactus: [" ", " n ____ n ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
21
+ robot: [" ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------´ "],
22
+ rabbit: [" ", " (\\__/) ", " ( {E} {E} ) ", " =( .. )= ", ' (")__(") '],
23
+ mushroom: [" ", " .-o-OO-o-. ", "(__________)", " |{E} {E}| ", " |____| "],
24
+ chonk: [" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------´ "],
25
+ };
26
+
27
+ const HAT_LINES: Record<string, string> = {
28
+ none: "",
29
+ crown: " \\^^^/ ",
30
+ tophat: " [___] ",
31
+ propeller: " -+- ",
32
+ halo: " ( ) ",
33
+ wizard: " /^\\ ",
34
+ beanie: " (___) ",
35
+ tinyduck: " ,> ",
36
+ };
37
+
38
+ export function renderSprite(roll: Roll): string[] {
39
+ const body = (BODIES[roll.species] ?? BODIES["blob"]!).map((line) =>
40
+ line.replaceAll("{E}", roll.eye),
41
+ );
42
+ const lines = [...body];
43
+
44
+ if (roll.hat !== "none" && !lines[0]!.trim()) {
45
+ lines[0] = HAT_LINES[roll.hat] ?? "";
46
+ }
47
+
48
+ // Drop blank hat row when no hat is present
49
+ if (!lines[0]!.trim()) {
50
+ lines.shift();
51
+ }
52
+
53
+ return lines;
54
+ }
package/src/types.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { RARITIES, STAT_NAMES } from "./enums.ts";
2
+
3
+ export type Rarity = (typeof RARITIES)[number];
4
+ export type StatName = (typeof STAT_NAMES)[number];
5
+
6
+ export type Roll = {
7
+ rarity: Rarity;
8
+ species: string;
9
+ eye: string;
10
+ hat: string;
11
+ shiny: boolean;
12
+ stats: Record<StatName, number>;
13
+ peak: StatName;
14
+ dump: StatName;
15
+ total: number;
16
+ };
17
+
18
+ export type SearchFilters = {
19
+ species?: string;
20
+ rarity?: Rarity;
21
+ eye?: string;
22
+ hat?: string;
23
+ shiny?: boolean;
24
+ peak?: StatName;
25
+ dump?: StatName;
26
+ minTotal?: number;
27
+ limit: number;
28
+ max: number;
29
+ };
30
+
31
+ export type SearchResult = Roll & { uuid: string };