jaml-ui 0.13.1 → 0.14.1

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.
Files changed (33) hide show
  1. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/playing_cards_metadata.json +230 -55
  2. package/dist/components/AnalyzerExplorer.js +75 -90
  3. package/dist/components/CardFan.js +2 -2
  4. package/dist/components/GameCard.js +9 -9
  5. package/dist/components/JamlAestheticSelector.d.ts +9 -0
  6. package/dist/components/JamlAestheticSelector.js +36 -0
  7. package/dist/components/JamlIde.d.ts +2 -1
  8. package/dist/components/JamlIde.js +6 -4
  9. package/dist/components/JamlIdeToolbar.js +1 -0
  10. package/dist/components/JamlSeedInput.d.ts +9 -0
  11. package/dist/components/JamlSeedInput.js +30 -0
  12. package/dist/components/JamlSpeedometer.d.ts +11 -0
  13. package/dist/components/JamlSpeedometer.js +54 -0
  14. package/dist/components/Standardcard.d.ts +18 -0
  15. package/dist/components/Standardcard.js +80 -0
  16. package/dist/decode/motelyItemDecoder.d.ts +3 -3
  17. package/dist/decode/motelyItemDecoder.js +12 -12
  18. package/dist/decode/packedBalatroItem.d.ts +1 -1
  19. package/dist/decode/packedBalatroItem.js +2 -2
  20. package/dist/hooks/searchWorkerCode.d.ts +1 -1
  21. package/dist/hooks/searchWorkerCode.js +31 -5
  22. package/dist/hooks/useSearch.d.ts +9 -0
  23. package/dist/hooks/useSearch.js +73 -17
  24. package/dist/index.d.ts +5 -1
  25. package/dist/index.js +5 -1
  26. package/dist/motely.d.ts +1 -1
  27. package/dist/motely.js +1 -1
  28. package/dist/ui/jimboTabs.js +1 -1
  29. package/dist/utils/fileSystem.d.ts +1 -0
  30. package/dist/utils/fileSystem.js +23 -0
  31. package/dist/utils/itemUtils.d.ts +1 -1
  32. package/dist/utils/itemUtils.js +3 -3
  33. package/package.json +4 -3
@@ -28,7 +28,7 @@ function normalizeCardSuit(raw) {
28
28
  return "Spades";
29
29
  return raw.trim();
30
30
  }
31
- function parsePlayingCardName(name) {
31
+ function parseStandardcardName(name) {
32
32
  const trimmed = name.trim();
33
33
  const ofMatch = /^(A|K|Q|J|10|[2-9]|Ace|King|Queen|Jack)\s+of\s+(Hearts|Clubs|Diamonds|Spades)$/i.exec(trimmed);
34
34
  if (ofMatch) {
@@ -137,15 +137,15 @@ function resolvePackedAnalyzerItem(item, scale) {
137
137
  return { kind: "voucher", voucherName: baseName };
138
138
  }
139
139
  }
140
- const playingCard = parsePlayingCardName(displayName) ?? parsePlayingCardName(baseName);
141
- if (playingCard) {
140
+ const standardcard = parseStandardcardName(displayName) ?? parseStandardcardName(baseName);
141
+ if (standardcard) {
142
142
  return {
143
143
  kind: "playing",
144
144
  type: "playing",
145
145
  card: {
146
146
  name: displayName,
147
- rank: playingCard.rank,
148
- suit: playingCard.suit,
147
+ rank: standardcard.rank,
148
+ suit: standardcard.suit,
149
149
  scale,
150
150
  },
151
151
  };
@@ -182,15 +182,15 @@ export function resolveAnalyzerShopItem(item, scale = 1) {
182
182
  return { kind: "voucher", voucherName: baseName };
183
183
  }
184
184
  }
185
- const playingCard = parsePlayingCardName(displayName) ?? parsePlayingCardName(baseName);
186
- if (playingCard) {
185
+ const standardcard = parseStandardcardName(displayName) ?? parseStandardcardName(baseName);
186
+ if (standardcard) {
187
187
  return {
188
188
  kind: "playing",
189
189
  type: "playing",
190
190
  card: {
191
191
  name: displayName,
192
- rank: playingCard.rank,
193
- suit: playingCard.suit,
192
+ rank: standardcard.rank,
193
+ suit: standardcard.suit,
194
194
  scale,
195
195
  },
196
196
  };
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ export type JamlAestheticOption = "Palindrome" | "Psychosis" | "Gross" | "Nsfw" | "Funny" | "Balatro";
3
+ export interface JamlAestheticSelectorProps {
4
+ value?: JamlAestheticOption | null;
5
+ onChange: (aesthetic: JamlAestheticOption | null, numericValue: number) => void;
6
+ className?: string;
7
+ style?: React.CSSProperties;
8
+ }
9
+ export declare function JamlAestheticSelector({ value, onChange, className, style }: JamlAestheticSelectorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { JimboColorOption } from "../ui/tokens.js";
4
+ import { JimboText } from "../ui/jimboText.js";
5
+ const AESTHETICS = [
6
+ { id: "Palindrome", value: 0, label: "Palindrome", desc: "Seeds that read the same forwards and backwards" },
7
+ { id: "Psychosis", value: 1, label: "Psychosis", desc: "Unsettling or eerie seed patterns" },
8
+ { id: "Gross", value: 2, label: "Gross", desc: "Seeds with crude or disgusting words" },
9
+ { id: "Nsfw", value: 3, label: "NSFW", desc: "Seeds with adult content" },
10
+ { id: "Funny", value: 4, label: "Funny", desc: "Seeds that spell funny words" },
11
+ { id: "Balatro", value: 5, label: "Balatro", desc: "Seeds referencing the game itself" },
12
+ ];
13
+ export function JamlAestheticSelector({ value, onChange, className, style }) {
14
+ return (_jsxs("div", { className: className, style: {
15
+ display: "flex",
16
+ flexDirection: "column",
17
+ gap: 4,
18
+ ...style,
19
+ }, children: [_jsx(JimboText, { size: "xs", tone: "grey", uppercase: true, children: "Seed Aesthetics" }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4 }, children: AESTHETICS.map((a) => {
20
+ const isActive = value === a.id;
21
+ return (_jsx("button", { type: "button", onClick: () => onChange(isActive ? null : a.id, a.value), title: a.desc, style: {
22
+ padding: "4px 10px",
23
+ borderRadius: 6,
24
+ border: `2px solid ${isActive ? JimboColorOption.GOLD : JimboColorOption.PANEL_EDGE}`,
25
+ background: isActive ? `${JimboColorOption.GOLD}22` : JimboColorOption.DARKEST,
26
+ color: isActive ? JimboColorOption.GOLD_TEXT : JimboColorOption.GREY,
27
+ cursor: "pointer",
28
+ fontSize: 11,
29
+ fontWeight: 700,
30
+ fontFamily: "m6x11plus, monospace",
31
+ textTransform: "uppercase",
32
+ letterSpacing: 0.5,
33
+ transition: "border-color 100ms, background 100ms",
34
+ }, children: a.label }, a.id));
35
+ }) })] }));
36
+ }
@@ -17,6 +17,7 @@ export interface JamlIdeProps {
17
17
  defaultMode?: JamlIdeMode;
18
18
  searchResults?: JamlIdeSearchResult[];
19
19
  className?: string;
20
+ style?: React.CSSProperties;
20
21
  title?: string;
21
22
  actions?: React.ReactNode;
22
23
  codePlaceholder?: string;
@@ -31,4 +32,4 @@ export interface JamlIdeProps {
31
32
  }
32
33
  export type { JamlVisualFilter } from "./JamlIdeVisual.js";
33
34
  export type { JamlVisualClause, JamlZone } from "./JamlIdeVisual.js";
34
- export declare function JamlIde({ jaml, defaultJaml, onChange, defaultMode, searchResults, className, title, actions, codePlaceholder, onSearch, isSearching, visualFilter, onVisualFilterChange, }: JamlIdeProps): import("react/jsx-runtime").JSX.Element;
35
+ export declare function JamlIde({ jaml, defaultJaml, onChange, defaultMode, searchResults, className, style, title, actions, codePlaceholder, onSearch, isSearching, visualFilter, onVisualFilterChange, }: JamlIdeProps): import("react/jsx-runtime").JSX.Element;
@@ -54,7 +54,7 @@ function ResultsView({ results }) {
54
54
  textAlign: "left",
55
55
  }, children: [_jsx("span", { style: {
56
56
  fontFamily: "m6x11plus, monospace",
57
- fontWeight: 700,
57
+ fontWeight: "normal",
58
58
  fontSize: 14,
59
59
  letterSpacing: 1,
60
60
  color: JimboColorOption.GOLD_TEXT,
@@ -65,7 +65,7 @@ function ResultsView({ results }) {
65
65
  color: result.score > 0 ? JimboColorOption.GREEN_TEXT : JimboColorOption.GREY,
66
66
  minWidth: 36,
67
67
  textAlign: "right",
68
- }, children: result.score })] })) : null, hasTally ? (_jsx("span", { style: { fontSize: 10, color: JimboColorOption.GREY, marginLeft: 2 }, children: isOpen ? "▲" : "▼" })) : null] }), isOpen && hasTally ? (_jsx("div", { style: {
68
+ }, children: result.score })] })) : null, hasTally ? (_jsx("span", { style: { fontSize: 11, color: JimboColorOption.GREY, marginLeft: 2 }, children: isOpen ? "▲" : "▼" })) : null] }), isOpen && hasTally ? (_jsx("div", { style: {
69
69
  borderTop: `1px solid ${JimboColorOption.PANEL_EDGE}`,
70
70
  padding: "8px 12px 10px",
71
71
  display: "flex",
@@ -91,7 +91,7 @@ function ResultsView({ results }) {
91
91
  }) })) : null] }, result.seed));
92
92
  }) }));
93
93
  }
94
- export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", searchResults = [], className = "", title = "JAML IDE", actions, codePlaceholder = "Enter JAML...", onSearch, isSearching = false, visualFilter, onVisualFilterChange, }) {
94
+ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", searchResults = [], className = "", style, title = "JAML IDE", actions, codePlaceholder = "Enter JAML...", onSearch, isSearching = false, visualFilter, onVisualFilterChange, }) {
95
95
  const [mode, setMode] = useState(defaultMode);
96
96
  const [internalText, setInternalText] = useState(jaml ?? defaultJaml ?? "");
97
97
  const [lastJamlProp, setLastJamlProp] = useState(jaml);
@@ -151,13 +151,15 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
151
151
  boxShadow: `0 3px 0 0 ${JimboColorOption.BORDER_SOUTH}`,
152
152
  background: JimboColorOption.DARK_GREY,
153
153
  color: JimboColorOption.WHITE,
154
+ ...style,
154
155
  }, children: [_jsxs("div", { style: {
155
156
  display: "flex",
156
157
  alignItems: "center",
157
158
  justifyContent: "space-between",
159
+ flexWrap: "wrap",
158
160
  gap: 12,
159
161
  padding: "10px 14px",
160
162
  borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
161
163
  background: JimboColorOption.TEAL_GREY,
162
- }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 14, fontWeight: 800, fontFamily: "m6x11plus, monospace", color: JimboColorOption.GOLD_TEXT }, children: title }), _jsx("div", { style: { fontSize: 11, color: JimboColorOption.GREY }, children: "Jimbo's Ante Markup Language" })] }), actions ? _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: actions }) : null] }), _jsx(JamlIdeToolbar, { mode: mode, onModeChange: setMode, resultCount: results.length, onSearch: onSearch, isSearching: isSearching }), _jsxs("div", { style: { flex: 1, minHeight: 0, overflow: "auto", background: JimboColorOption.DARKEST }, children: [mode === "visual" ? (_jsx(JamlIdeVisual, { filter: activeFilter, onChange: handleVisualFilterChange })) : null, mode === "code" ? (_jsx(JamlCodeEditor, { value: text, onChange: handleTextChange, placeholder: codePlaceholder })) : null, mode === "map" ? _jsx(JamlMapPreview, { jaml: text }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results }) })) : null] })] }));
164
+ }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 16, fontWeight: "normal", fontFamily: "m6x11plus, monospace", color: JimboColorOption.GOLD_TEXT }, children: title }), _jsx("div", { style: { fontSize: 11, color: JimboColorOption.GREY }, children: "Jimbo's Ante Markup Language" })] }), actions ? _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: actions }) : null] }), _jsx(JamlIdeToolbar, { mode: mode, onModeChange: setMode, resultCount: results.length, onSearch: onSearch, isSearching: isSearching }), _jsxs("div", { style: { flex: 1, minHeight: 0, overflow: "auto", background: JimboColorOption.DARKEST }, children: [mode === "visual" ? (_jsx(JamlIdeVisual, { filter: activeFilter, onChange: handleVisualFilterChange })) : null, mode === "code" ? (_jsx(JamlCodeEditor, { value: text, onChange: handleTextChange, placeholder: codePlaceholder })) : null, mode === "map" ? _jsx(JamlMapPreview, { jaml: text }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results }) })) : null] })] }));
163
165
  }
@@ -14,6 +14,7 @@ export function JamlIdeToolbar({ mode, onModeChange, resultCount = 0, className
14
14
  display: "flex",
15
15
  alignItems: "center",
16
16
  justifyContent: "space-between",
17
+ flexWrap: "wrap",
17
18
  gap: 8,
18
19
  padding: "10px 10px 6px",
19
20
  borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ export interface JamlSeedInputProps {
3
+ value?: string;
4
+ onChange?: (seed: string) => void;
5
+ placeholder?: string;
6
+ className?: string;
7
+ style?: React.CSSProperties;
8
+ }
9
+ export declare function JamlSeedInput({ value, onChange, placeholder, className, style }: JamlSeedInputProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { JimboColorOption } from "../ui/tokens.js";
5
+ import { JimboText } from "../ui/jimboText.js";
6
+ const SEED_PATTERN = /^[A-Z0-9]{0,8}$/;
7
+ export function JamlSeedInput({ value, onChange, placeholder = "Enter seed (e.g. J4SPZMWW)", className, style }) {
8
+ const [internal, setInternal] = useState(value ?? "");
9
+ const display = value ?? internal;
10
+ const isValid = display.length === 0 || SEED_PATTERN.test(display);
11
+ const handleChange = (e) => {
12
+ const raw = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 8);
13
+ setInternal(raw);
14
+ onChange?.(raw);
15
+ };
16
+ return (_jsxs("div", { className: className, style: { display: "flex", flexDirection: "column", gap: 4, ...style }, children: [_jsx(JimboText, { size: "xs", tone: "grey", uppercase: true, children: "Seed" }), _jsx("input", { type: "text", value: display, onChange: handleChange, placeholder: placeholder, maxLength: 8, spellCheck: false, autoComplete: "off", style: {
17
+ padding: "6px 10px",
18
+ borderRadius: 6,
19
+ border: `2px solid ${!isValid ? JimboColorOption.RED : display.length === 8 ? JimboColorOption.GREEN : JimboColorOption.PANEL_EDGE}`,
20
+ background: JimboColorOption.DARKEST,
21
+ color: JimboColorOption.GOLD_TEXT,
22
+ fontSize: 16,
23
+ fontWeight: 900,
24
+ fontFamily: "m6x11plus, monospace",
25
+ letterSpacing: 2,
26
+ textTransform: "uppercase",
27
+ outline: "none",
28
+ transition: "border-color 100ms",
29
+ } }), display.length > 0 && display.length < 8 && (_jsxs(JimboText, { size: "xs", tone: "grey", children: [8 - display.length, " more characters"] }))] }));
30
+ }
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import type { SearchStatus } from "../hooks/useSearch.js";
3
+ export interface JamlSpeedometerProps {
4
+ seedsPerSecond: number;
5
+ totalSearched: bigint;
6
+ matchingSeeds: bigint;
7
+ status: SearchStatus;
8
+ className?: string;
9
+ style?: React.CSSProperties;
10
+ }
11
+ export declare function JamlSpeedometer({ seedsPerSecond, totalSearched, matchingSeeds, status, className, style, }: JamlSpeedometerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { JimboColorOption } from "../ui/tokens.js";
4
+ import { JimboText } from "../ui/jimboText.js";
5
+ function formatCount(n) {
6
+ if (n >= 1000000000n)
7
+ return `${(Number(n / 1000000n) / 1000).toFixed(1)}B`;
8
+ if (n >= 1000000n)
9
+ return `${(Number(n / 1000n) / 1000).toFixed(1)}M`;
10
+ if (n >= 1000n)
11
+ return `${(Number(n) / 1000).toFixed(1)}K`;
12
+ return n.toString();
13
+ }
14
+ function formatSpeed(sps) {
15
+ if (sps >= 1_000_000)
16
+ return `${(sps / 1_000_000).toFixed(1)}M`;
17
+ if (sps >= 1_000)
18
+ return `${(sps / 1_000).toFixed(0)}K`;
19
+ return sps.toString();
20
+ }
21
+ function needleAngle(sps) {
22
+ if (sps <= 0)
23
+ return -90;
24
+ const maxLog = Math.log10(5_000_000);
25
+ const clamped = Math.min(sps, 5_000_000);
26
+ const pct = Math.log10(Math.max(clamped, 1)) / maxLog;
27
+ return -90 + pct * 180;
28
+ }
29
+ export function JamlSpeedometer({ seedsPerSecond, totalSearched, matchingSeeds, status, className, style, }) {
30
+ const isActive = status === "running" || status === "booting";
31
+ const angle = needleAngle(seedsPerSecond);
32
+ const speedColor = seedsPerSecond >= 500_000
33
+ ? JimboColorOption.GREEN
34
+ : seedsPerSecond >= 100_000
35
+ ? JimboColorOption.GOLD
36
+ : seedsPerSecond > 0
37
+ ? JimboColorOption.ORANGE
38
+ : JimboColorOption.GREY;
39
+ return (_jsxs("div", { className: className, style: {
40
+ display: "flex",
41
+ flexDirection: "column",
42
+ alignItems: "center",
43
+ gap: 6,
44
+ padding: "12px 16px",
45
+ borderRadius: 10,
46
+ background: `${JimboColorOption.DARKEST}cc`,
47
+ border: `1px solid ${JimboColorOption.PANEL_EDGE}`,
48
+ ...style,
49
+ }, children: [_jsx("div", { style: { position: "relative", width: 120, height: 68, overflow: "hidden" }, children: _jsxs("svg", { viewBox: "0 0 120 68", width: 120, height: 68, children: [_jsx("path", { d: "M 10 65 A 50 50 0 0 1 110 65", fill: "none", stroke: JimboColorOption.DARK_GREY, strokeWidth: 6, strokeLinecap: "round" }), isActive && (_jsx("path", { d: "M 10 65 A 50 50 0 0 1 110 65", fill: "none", stroke: speedColor, strokeWidth: 6, strokeLinecap: "round", strokeDasharray: "157", strokeDashoffset: 157 - (157 * ((angle + 90) / 180)), style: { transition: "stroke-dashoffset 300ms ease, stroke 300ms ease" } })), _jsx("line", { x1: 60, y1: 65, x2: 60, y2: 20, stroke: isActive ? JimboColorOption.RED : JimboColorOption.GREY, strokeWidth: 2, strokeLinecap: "round", style: {
50
+ transformOrigin: "60px 65px",
51
+ transform: `rotate(${angle}deg)`,
52
+ transition: "transform 300ms ease",
53
+ } }), _jsx("circle", { cx: 60, cy: 65, r: 4, fill: JimboColorOption.RED })] }) }), _jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 20, fontWeight: 900, fontFamily: "m6x11plus, monospace", color: isActive ? speedColor : JimboColorOption.GREY }, children: isActive ? formatSpeed(seedsPerSecond) : "---" }), _jsx(JimboText, { size: "xs", tone: "grey", uppercase: true, children: "seeds / sec" })] }), _jsxs("div", { style: { display: "flex", gap: 16, marginTop: 2 }, children: [_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 13, fontWeight: 700, fontFamily: "m6x11plus, monospace", color: JimboColorOption.WHITE }, children: formatCount(totalSearched) }), _jsx(JimboText, { size: "xs", tone: "grey", uppercase: true, children: "searched" })] }), _jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 13, fontWeight: 700, fontFamily: "m6x11plus, monospace", color: JimboColorOption.GREEN_TEXT }, children: formatCount(matchingSeeds) }), _jsx(JimboText, { size: "xs", tone: "grey", uppercase: true, children: "matches" })] })] })] }));
54
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ export type CardSuit = 'Hearts' | 'Diamonds' | 'Clubs' | 'Spades' | 'hearts' | 'diamonds' | 'clubs' | 'spades';
3
+ export type CardRank = 'Ace' | 'King' | 'Queen' | 'Jack' | '10' | '9' | '8' | '7' | '6' | '5' | '4' | '3' | '2' | 'A' | 'K' | 'Q' | 'J';
4
+ export type CardEnhancement = 'bonus' | 'mult' | 'wild' | 'glass' | 'steel' | 'stone' | 'gold' | 'lucky' | null;
5
+ export type CardSeal = 'gold' | 'red' | 'blue' | 'purple' | null;
6
+ export type CardEdition = 'Foil' | 'Holographic' | 'Polychrome' | 'Negative' | null;
7
+ interface RealStandardcardProps {
8
+ suit: CardSuit;
9
+ rank: CardRank;
10
+ enhancement?: CardEnhancement;
11
+ seal?: CardSeal;
12
+ edition?: CardEdition;
13
+ className?: string;
14
+ size?: number;
15
+ style?: React.CSSProperties;
16
+ }
17
+ export declare function RealStandardcard({ suit, rank, enhancement, seal, edition, className, size, style }: RealStandardcardProps): import("react/jsx-runtime").JSX.Element | null;
18
+ export {};
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { resolveJamlAssetUrl } from '../assets.js';
4
+ import { RANK_MAP, SUIT_MAP, ENHANCER_MAP, SEAL_MAP, EDITION_MAP } from '../sprites/spriteData.js';
5
+ function cn(...classes) { return classes.filter(Boolean).join(" "); }
6
+ const CARD_WIDTH = 71;
7
+ const CARD_HEIGHT = 95;
8
+ const RANK_ALIAS = { A: 'Ace', K: 'King', Q: 'Queen', J: 'Jack' };
9
+ const pascal = (s) => s[0].toUpperCase() + s.slice(1).toLowerCase();
10
+ export function RealStandardcard({ suit, rank, enhancement, seal, edition, className, size = 71, style }) {
11
+ const rankKey = RANK_ALIAS[rank] ?? rank;
12
+ const suitKey = pascal(suit);
13
+ const col = RANK_MAP[rankKey];
14
+ const row = SUIT_MAP[suitKey];
15
+ if (col === undefined || row === undefined) {
16
+ console.warn(`Invalid card: ${rank} of ${suit}`);
17
+ return null;
18
+ }
19
+ const scale = size / CARD_WIDTH;
20
+ const finalH = size * (CARD_HEIGHT / CARD_WIDTH);
21
+ // Base card position
22
+ const bgX = -col * CARD_WIDTH;
23
+ const bgY = -row * CARD_HEIGHT;
24
+ // Enhancement background (if any) — ENHANCER_MAP is PascalCase, prop is lowercase
25
+ const enhPos = enhancement ? ENHANCER_MAP[pascal(enhancement)] ?? { x: 0, y: 0 } : { x: 0, y: 0 };
26
+ const enhBgX = -enhPos.x * CARD_WIDTH;
27
+ const enhBgY = -enhPos.y * CARD_HEIGHT;
28
+ // Seal overlay — SEAL_MAP is keyed by "Gold"/"Red"/"Blue"/"Purple"
29
+ const sealPos = seal ? SEAL_MAP[pascal(seal)] ?? null : null;
30
+ const sealBgX = sealPos ? -sealPos.x * CARD_WIDTH : 0;
31
+ const sealBgY = sealPos ? -sealPos.y * CARD_HEIGHT : 0;
32
+ // Edition overlay — EDITION_MAP gives column index on 5-wide editions sheet
33
+ const editionCol = edition ? EDITION_MAP[edition] : undefined;
34
+ const editionBgX = editionCol !== undefined ? -editionCol * CARD_WIDTH : 0;
35
+ const editionBgY = 0;
36
+ const isNegative = edition === 'Negative';
37
+ const baseFilter = isNegative ? 'invert(0.94)' : 'none';
38
+ const enhancersUrl = resolveJamlAssetUrl('enhancers');
39
+ const deckUrl = resolveJamlAssetUrl('deck');
40
+ const editionsUrl = resolveJamlAssetUrl('editions');
41
+ return (_jsxs("div", { className: cn('relative overflow-hidden inline-block select-none', className), style: {
42
+ width: size,
43
+ height: finalH,
44
+ imageRendering: 'pixelated',
45
+ ...style
46
+ }, title: `${rank} of ${suit}${enhancement ? ` (${enhancement})` : ''}${seal ? ` [${seal} seal]` : ''}${edition ? ` {${edition}}` : ''}`, children: [_jsx("div", { className: "absolute inset-0", style: {
47
+ backgroundImage: `url(${enhancersUrl})`,
48
+ backgroundPosition: `${enhBgX}px ${enhBgY}px`,
49
+ width: CARD_WIDTH,
50
+ height: CARD_HEIGHT,
51
+ transform: `scale(${scale})`,
52
+ transformOrigin: 'top left',
53
+ backgroundRepeat: 'no-repeat',
54
+ } }), _jsx("div", { className: "absolute inset-0 z-[1]", style: {
55
+ backgroundImage: `url(${deckUrl})`,
56
+ backgroundPosition: `${bgX}px ${bgY}px`,
57
+ width: CARD_WIDTH,
58
+ height: CARD_HEIGHT,
59
+ transform: `scale(${scale})`,
60
+ transformOrigin: 'top left',
61
+ backgroundRepeat: 'no-repeat',
62
+ filter: baseFilter
63
+ } }), edition && edition !== 'Negative' && (_jsx("div", { className: "absolute inset-0 z-[2] mix-blend-screen opacity-60", style: {
64
+ backgroundImage: `url(${editionsUrl})`,
65
+ backgroundPosition: `${editionBgX}px ${editionBgY}px`,
66
+ width: CARD_WIDTH,
67
+ height: CARD_HEIGHT,
68
+ transform: `scale(${scale})`,
69
+ transformOrigin: 'top left',
70
+ backgroundRepeat: 'no-repeat',
71
+ } })), seal && (_jsx("div", { className: "absolute inset-0 z-[3]", style: {
72
+ backgroundImage: `url(${enhancersUrl})`,
73
+ backgroundPosition: `${sealBgX}px ${sealBgY}px`,
74
+ width: CARD_WIDTH,
75
+ height: CARD_HEIGHT,
76
+ transform: `scale(${scale})`,
77
+ transformOrigin: 'top left',
78
+ backgroundRepeat: 'no-repeat',
79
+ } })), isNegative && (_jsx("div", { className: "absolute inset-0 z-[4] bg-red-500/10 pointer-events-none mix-blend-overlay" }))] }));
80
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * MotelyItem.Value is a packed integer. The MotelyItemType enum
5
5
  * uses packed integers where the top nibble encodes category:
6
- * 0x1000 = PlayingCard, 0x2000 = Spectral, 0x3000 = Tarot,
6
+ * 0x1000 = Standardcard, 0x2000 = Spectral, 0x3000 = Tarot,
7
7
  * 0x4000 = Planet, 0x5000 = Joker, 0xF000 = Invalid
8
8
  */
9
9
  import { type CardCategory } from "../utils/itemUtils.js";
@@ -46,8 +46,8 @@ export declare function motelyItemRenderCategory(input: MotelyItemInput): Motely
46
46
  export declare function motelyItemEditionName(input: MotelyItemInput): "Foil" | "Holographic" | "Polychrome" | "Negative" | null;
47
47
  export declare function motelyItemSealName(input: MotelyItemInput): "Gold" | "Red" | "Blue" | "Purple" | null;
48
48
  export declare function motelyItemEnhancementName(input: MotelyItemInput): string | null;
49
- export declare function motelyPlayingCardSuitName(input: MotelyItemInput): "Clubs" | "Diamonds" | "Hearts" | "Spades" | null;
50
- export declare function motelyPlayingCardRankName(input: MotelyItemInput): string | null;
49
+ export declare function motelyStandardcardSuitName(input: MotelyItemInput): "Clubs" | "Diamonds" | "Hearts" | "Spades" | null;
50
+ export declare function motelyStandardcardRankName(input: MotelyItemInput): string | null;
51
51
  /** Get the enum key name for a MotelyItemType value. */
52
52
  export declare function motelyItemTypeName(input: MotelyItemInput): string;
53
53
  /** Get the category string for a MotelyItemType value. */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * MotelyItem.Value is a packed integer. The MotelyItemType enum
5
5
  * uses packed integers where the top nibble encodes category:
6
- * 0x1000 = PlayingCard, 0x2000 = Spectral, 0x3000 = Tarot,
6
+ * 0x1000 = Standardcard, 0x2000 = Spectral, 0x3000 = Tarot,
7
7
  * 0x4000 = Planet, 0x5000 = Joker, 0xF000 = Invalid
8
8
  */
9
9
  import { Motely } from "motely-wasm";
@@ -15,7 +15,7 @@ const VALUE_SEAL_MASK = 0x70000;
15
15
  const VALUE_ENHANCEMENT_MASK = 0x780000;
16
16
  const VALUE_EDITION_MASK = 0x3800000;
17
17
  const CATEGORY_TO_TYPE = {
18
- 0x1000: "Playing Card",
18
+ 0x1000: "Standard card",
19
19
  0x2000: "Spectral",
20
20
  0x3000: "Tarot",
21
21
  0x4000: "Planet",
@@ -45,7 +45,7 @@ function runtimeEnumName(enumObject, value) {
45
45
  const enumKey = enumObject[String(value)];
46
46
  return typeof enumKey === "string" && enumKey.length > 0 ? enumKey : null;
47
47
  }
48
- function parsePlayingCardEnumKey(enumKey) {
48
+ function parseStandardcardEnumKey(enumKey) {
49
49
  const match = /^([CDHS])(10|[2-9JQKA])$/.exec(enumKey);
50
50
  if (!match)
51
51
  return null;
@@ -142,22 +142,22 @@ export function motelyItemEnhancementName(input) {
142
142
  const enumKey = runtimeEnumName(Motely.MotelyItemEnhancement, resolveEnhancementValue(input));
143
143
  return enumKey === null || enumKey === "None" ? null : enumKey;
144
144
  }
145
- export function motelyPlayingCardSuitName(input) {
145
+ export function motelyStandardcardSuitName(input) {
146
146
  const runtimeItem = asRuntimeItem(input);
147
- const directSuit = runtimeEnumName(Motely.MotelyPlayingCardSuit, finiteNumber(runtimeItem?.suit));
147
+ const directSuit = runtimeEnumName(Motely.MotelyStandardcardSuit, finiteNumber(runtimeItem?.suit));
148
148
  if (directSuit === "Clubs" || directSuit === "Diamonds" || directSuit === "Hearts" || directSuit === "Spades") {
149
149
  return directSuit;
150
150
  }
151
- const parsed = parsePlayingCardEnumKey(motelyItemTypeName(input));
151
+ const parsed = parseStandardcardEnumKey(motelyItemTypeName(input));
152
152
  return parsed?.suit ?? null;
153
153
  }
154
- export function motelyPlayingCardRankName(input) {
154
+ export function motelyStandardcardRankName(input) {
155
155
  const runtimeItem = asRuntimeItem(input);
156
- const directRank = runtimeEnumName(Motely.MotelyPlayingCardRank, finiteNumber(runtimeItem?.rank));
156
+ const directRank = runtimeEnumName(Motely.MotelyStandardcardRank, finiteNumber(runtimeItem?.rank));
157
157
  const normalizedDirect = rankNameFromEnum(directRank);
158
158
  if (normalizedDirect !== null)
159
159
  return normalizedDirect;
160
- const parsed = parsePlayingCardEnumKey(motelyItemTypeName(input));
160
+ const parsed = parseStandardcardEnumKey(motelyItemTypeName(input));
161
161
  return parsed?.rank ?? null;
162
162
  }
163
163
  /** Get the enum key name for a MotelyItemType value. */
@@ -175,7 +175,7 @@ export function motelyItemCategory(input) {
175
175
  return "Unknown";
176
176
  const renderCategory = motelyItemRenderCategory(itemType);
177
177
  if (renderCategory === "playing")
178
- return "Playing Card";
178
+ return "Standard card";
179
179
  if (renderCategory === "spectral")
180
180
  return "Spectral";
181
181
  if (renderCategory === "tarot")
@@ -206,8 +206,8 @@ export function decodeMotelyItem(input) {
206
206
  return null;
207
207
  }
208
208
  const category = motelyItemRenderCategory(itemType);
209
- const rank = motelyPlayingCardRankName(itemType);
210
- const suit = motelyPlayingCardSuitName(itemType);
209
+ const rank = motelyStandardcardRankName(itemType);
210
+ const suit = motelyStandardcardSuitName(itemType);
211
211
  base = {
212
212
  itemType,
213
213
  enumKey,
@@ -1,6 +1,6 @@
1
1
  /** Bit-packed shop/card ids (Balatro item encoding). */
2
2
  export declare const BalatroItemCategory: {
3
- readonly PlayingCard: 1;
3
+ readonly Standardcard: 1;
4
4
  readonly Spectral: 2;
5
5
  readonly Tarot: 3;
6
6
  readonly Planet: 4;
@@ -1,6 +1,6 @@
1
1
  /** Bit-packed shop/card ids (Balatro item encoding). */
2
2
  export const BalatroItemCategory = {
3
- PlayingCard: 1,
3
+ Standardcard: 1,
4
4
  Spectral: 2,
5
5
  Tarot: 3,
6
6
  Planet: 4,
@@ -22,5 +22,5 @@ export function packedItemIndex(packed) {
22
22
  }
23
23
  export function isPackedItemValid(packed) {
24
24
  const category = packedItemCategory(packed);
25
- return category >= BalatroItemCategory.PlayingCard && category <= BalatroItemCategory.Joker;
25
+ return category >= BalatroItemCategory.Standardcard && category <= BalatroItemCategory.Joker;
26
26
  }
@@ -1 +1 @@
1
- export declare const SEARCH_WORKER_CODE = "\nlet MotelyWasm = null;\nlet MotelyWasmEvents = null;\nlet activeSearch = null;\n\nself.addEventListener('message', async function(e) {\n const msg = e.data;\n\n if (msg.type === 'init') {\n try {\n const mod = await import(msg.url);\n await mod.default.boot();\n MotelyWasm = mod.MotelyWasm;\n MotelyWasmEvents = mod.MotelyWasmEvents;\n self.postMessage({ type: 'ready' });\n } catch (err) {\n self.postMessage({ type: 'error', message: String(err) });\n }\n return;\n }\n\n if (msg.type === 'start') {\n if (!MotelyWasm) { self.postMessage({ type: 'error', message: 'Not initialized' }); return; }\n const validation = MotelyWasm.validateJaml(msg.jaml);\n if (validation !== 'valid') { self.postMessage({ type: 'error', message: validation }); return; }\n\n let rId, pId, cId;\n function cleanup() {\n MotelyWasmEvents.onResult.unsubscribeById(rId);\n MotelyWasmEvents.onProgress.unsubscribeById(pId);\n MotelyWasmEvents.onComplete.unsubscribeById(cId);\n activeSearch = null;\n }\n\n rId = MotelyWasmEvents.onResult.subscribe(function(seed, score) {\n self.postMessage({ type: 'result', seed, score });\n });\n pId = MotelyWasmEvents.onProgress.subscribe(function(searched, matching) {\n self.postMessage({ type: 'progress', searched: searched.toString(), matching: matching.toString() });\n });\n cId = MotelyWasmEvents.onComplete.subscribe(function(status, searched, matched) {\n cleanup();\n self.postMessage({ type: 'complete', status, searched: searched.toString(), matched: matched.toString() });\n });\n\n try {\n activeSearch = MotelyWasm.startRandomSearch(msg.jaml, msg.count);\n } catch (err) {\n cleanup();\n self.postMessage({ type: 'error', message: String(err) });\n }\n return;\n }\n\n if (msg.type === 'stop') {\n if (activeSearch) { activeSearch.cancel(); activeSearch = null; }\n self.postMessage({ type: 'cancelled' });\n }\n});\n";
1
+ export declare const SEARCH_WORKER_CODE = "\nlet MotelyWasm = null;\nlet MotelyWasmEvents = null;\nlet Filters = null;\nlet activeSearch = null;\n\nself.addEventListener('message', async function(e) {\n const msg = e.data;\n\n if (msg.type === 'init') {\n try {\n const mod = await import(msg.url);\n await mod.default.boot();\n MotelyWasm = mod.MotelyWasm;\n MotelyWasmEvents = mod.MotelyWasmEvents;\n Filters = mod.Filters;\n self.postMessage({ type: 'ready' });\n } catch (err) {\n self.postMessage({ type: 'error', message: String(err) });\n }\n return;\n }\n\n if (msg.type === 'start') {\n if (!MotelyWasm) { self.postMessage({ type: 'error', message: 'Not initialized' }); return; }\n const validation = MotelyWasm.validateJaml(msg.jaml);\n if (validation !== 'valid') { self.postMessage({ type: 'error', message: validation }); return; }\n\n let rId, pId, cId;\n function cleanup() {\n MotelyWasmEvents.onResult.unsubscribeById(rId);\n MotelyWasmEvents.onProgress.unsubscribeById(pId);\n MotelyWasmEvents.onComplete.unsubscribeById(cId);\n activeSearch = null;\n }\n\n rId = MotelyWasmEvents.onResult.subscribe(function(seed, score, tallyColumns) {\n self.postMessage({ type: 'result', seed, score, tallyColumns: Array.from(tallyColumns) });\n });\n pId = MotelyWasmEvents.onProgress.subscribe(function(searched, matching) {\n self.postMessage({ type: 'progress', searched: searched.toString(), matching: matching.toString() });\n });\n cId = MotelyWasmEvents.onComplete.subscribe(function(status, searched, matched) {\n cleanup();\n self.postMessage({ type: 'complete', status, searched: searched.toString(), matched: matched.toString() });\n });\n\n try {\n const mode = msg.mode || 'random';\n\n if (mode === 'random') {\n activeSearch = MotelyWasm.startRandomSearch(msg.jaml, msg.count);\n } else if (mode === 'aesthetic') {\n activeSearch = MotelyWasm.startAestheticSearch(msg.jaml, msg.aesthetic);\n } else if (mode === 'seedList') {\n activeSearch = MotelyWasm.startSeedListSearch(msg.jaml, msg.seeds);\n } else if (mode === 'keyword') {\n activeSearch = MotelyWasm.startKeywordSearch(msg.jaml, msg.keywords, msg.padding || '');\n } else if (mode === 'sequential') {\n activeSearch = MotelyWasm.startSequentialSearch(msg.jaml, msg.batchCharCount, BigInt(msg.startBatch), BigInt(msg.endBatch));\n } else {\n self.postMessage({ type: 'error', message: 'Unknown search mode: ' + mode });\n cleanup();\n return;\n }\n } catch (err) {\n cleanup();\n self.postMessage({ type: 'error', message: String(err) });\n }\n return;\n }\n\n if (msg.type === 'stop') {\n if (activeSearch) { activeSearch.cancel(); activeSearch = null; }\n self.postMessage({ type: 'cancelled' });\n }\n\n if (msg.type === 'get_tally_labels') {\n if (!MotelyWasm) { self.postMessage({ type: 'error', message: 'Not initialized' }); return; }\n try {\n const labels = MotelyWasm.getTallyLabels(msg.jaml);\n self.postMessage({ type: 'tally_labels', labels });\n } catch (err) {\n self.postMessage({ type: 'error', message: String(err) });\n }\n }\n});\n";
@@ -1,8 +1,7 @@
1
- // Worker code as an inline string — created as a Blob URL at runtime.
2
- // This avoids bundler/import.meta.url issues when shipped as an npm package.
3
1
  export const SEARCH_WORKER_CODE = `
4
2
  let MotelyWasm = null;
5
3
  let MotelyWasmEvents = null;
4
+ let Filters = null;
6
5
  let activeSearch = null;
7
6
 
8
7
  self.addEventListener('message', async function(e) {
@@ -14,6 +13,7 @@ self.addEventListener('message', async function(e) {
14
13
  await mod.default.boot();
15
14
  MotelyWasm = mod.MotelyWasm;
16
15
  MotelyWasmEvents = mod.MotelyWasmEvents;
16
+ Filters = mod.Filters;
17
17
  self.postMessage({ type: 'ready' });
18
18
  } catch (err) {
19
19
  self.postMessage({ type: 'error', message: String(err) });
@@ -34,8 +34,8 @@ self.addEventListener('message', async function(e) {
34
34
  activeSearch = null;
35
35
  }
36
36
 
37
- rId = MotelyWasmEvents.onResult.subscribe(function(seed, score) {
38
- self.postMessage({ type: 'result', seed, score });
37
+ rId = MotelyWasmEvents.onResult.subscribe(function(seed, score, tallyColumns) {
38
+ self.postMessage({ type: 'result', seed, score, tallyColumns: Array.from(tallyColumns) });
39
39
  });
40
40
  pId = MotelyWasmEvents.onProgress.subscribe(function(searched, matching) {
41
41
  self.postMessage({ type: 'progress', searched: searched.toString(), matching: matching.toString() });
@@ -46,7 +46,23 @@ self.addEventListener('message', async function(e) {
46
46
  });
47
47
 
48
48
  try {
49
- activeSearch = MotelyWasm.startRandomSearch(msg.jaml, msg.count);
49
+ const mode = msg.mode || 'random';
50
+
51
+ if (mode === 'random') {
52
+ activeSearch = MotelyWasm.startRandomSearch(msg.jaml, msg.count);
53
+ } else if (mode === 'aesthetic') {
54
+ activeSearch = MotelyWasm.startAestheticSearch(msg.jaml, msg.aesthetic);
55
+ } else if (mode === 'seedList') {
56
+ activeSearch = MotelyWasm.startSeedListSearch(msg.jaml, msg.seeds);
57
+ } else if (mode === 'keyword') {
58
+ activeSearch = MotelyWasm.startKeywordSearch(msg.jaml, msg.keywords, msg.padding || '');
59
+ } else if (mode === 'sequential') {
60
+ activeSearch = MotelyWasm.startSequentialSearch(msg.jaml, msg.batchCharCount, BigInt(msg.startBatch), BigInt(msg.endBatch));
61
+ } else {
62
+ self.postMessage({ type: 'error', message: 'Unknown search mode: ' + mode });
63
+ cleanup();
64
+ return;
65
+ }
50
66
  } catch (err) {
51
67
  cleanup();
52
68
  self.postMessage({ type: 'error', message: String(err) });
@@ -58,5 +74,15 @@ self.addEventListener('message', async function(e) {
58
74
  if (activeSearch) { activeSearch.cancel(); activeSearch = null; }
59
75
  self.postMessage({ type: 'cancelled' });
60
76
  }
77
+
78
+ if (msg.type === 'get_tally_labels') {
79
+ if (!MotelyWasm) { self.postMessage({ type: 'error', message: 'Not initialized' }); return; }
80
+ try {
81
+ const labels = MotelyWasm.getTallyLabels(msg.jaml);
82
+ self.postMessage({ type: 'tally_labels', labels });
83
+ } catch (err) {
84
+ self.postMessage({ type: 'error', message: String(err) });
85
+ }
86
+ }
61
87
  });
62
88
  `;
@@ -1,6 +1,7 @@
1
1
  export interface SearchResult {
2
2
  seed: string;
3
3
  score: number;
4
+ tallyColumns?: number[];
4
5
  }
5
6
  export type SearchStatus = "idle" | "booting" | "running" | "completed" | "cancelled" | "error";
6
7
  export interface UseSearchState {
@@ -9,14 +10,22 @@ export interface UseSearchState {
9
10
  matchingSeeds: bigint;
10
11
  status: SearchStatus;
11
12
  error: string | null;
13
+ seedsPerSecond: number;
14
+ tallyLabels: string[];
12
15
  }
13
16
  export declare function useSearch(motelyWasmUrl: string): {
14
17
  start: (jaml: string, count: number) => void;
18
+ startAesthetic: (jaml: string, aesthetic: number) => void;
19
+ startSeedList: (jaml: string, seeds: string[]) => void;
20
+ startKeyword: (jaml: string, keywords: string, padding?: string) => void;
15
21
  cancel: () => void;
16
22
  clearError: () => void;
23
+ fetchTallyLabels: (jaml: string) => void;
17
24
  results: SearchResult[];
18
25
  totalSearched: bigint;
19
26
  matchingSeeds: bigint;
20
27
  status: SearchStatus;
21
28
  error: string | null;
29
+ seedsPerSecond: number;
30
+ tallyLabels: string[];
22
31
  };