jaml-ui 0.11.0 → 0.13.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.
- package/dist/components/JamlAnalyzerFullscreen.d.ts +21 -0
- package/dist/components/JamlAnalyzerFullscreen.js +368 -0
- package/dist/components/JamlCodeEditor.js +69 -1
- package/dist/components/JamlIde.js +4 -2
- package/dist/components/JamlIdeToolbar.js +9 -49
- package/dist/hooks/analyzerStreamRegistry.d.ts +35 -0
- package/dist/hooks/analyzerStreamRegistry.js +148 -0
- package/dist/hooks/useAnalyzer.d.ts +22 -0
- package/dist/hooks/useAnalyzer.js +6 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AnalyzerAnteView, AnalyzerItem } from "./AnalyzerExplorer.js";
|
|
2
|
+
import type { AnalyzerLive } from "../hooks/useAnalyzer.js";
|
|
3
|
+
import { type AnalyzerStreamKey } from "../hooks/analyzerStreamRegistry.js";
|
|
4
|
+
export interface JamlAnalyzerFullscreenProps {
|
|
5
|
+
/** Per-ante summaries from useAnalyzer.antes. */
|
|
6
|
+
antes: AnalyzerAnteView[];
|
|
7
|
+
/** Live ctx from useAnalyzer.live; null disables additional stream lanes. */
|
|
8
|
+
live: AnalyzerLive | null;
|
|
9
|
+
/** Stream lanes to surface. Defaults to shop + soul jokers. */
|
|
10
|
+
enabledStreams?: AnalyzerStreamKey[];
|
|
11
|
+
/** Called when the user toggles a stream in the picker. Owners persist if desired. */
|
|
12
|
+
onEnabledStreamsChange?: (next: AnalyzerStreamKey[]) => void;
|
|
13
|
+
/** Hide the built-in stream picker overlay (e.g. when host renders its own). */
|
|
14
|
+
hidePicker?: boolean;
|
|
15
|
+
/** Pull size on each lazy load. */
|
|
16
|
+
chunkSize?: number;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function JamlAnalyzerFullscreen({ antes, live, enabledStreams, onEnabledStreamsChange, hidePicker, chunkSize, className, }: JamlAnalyzerFullscreenProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export type { AnalyzerItem };
|
|
21
|
+
export { ANALYZER_STREAM_META, type AnalyzerStreamKey } from "../hooks/analyzerStreamRegistry.js";
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { JamlBoss, JamlGameCard, JamlTag, JamlVoucher, resolveAnalyzerShopItem } from "./GameCard.js";
|
|
5
|
+
import { useMotelyStream } from "../hooks/useShopStream.js";
|
|
6
|
+
import { ANALYZER_STREAM_META, DEFAULT_ENABLED_STREAMS, buildStreamHandle, } from "../hooks/analyzerStreamRegistry.js";
|
|
7
|
+
import { JimboColorOption, withAlpha } from "../ui/tokens.js";
|
|
8
|
+
const C = JimboColorOption;
|
|
9
|
+
const TONE_COLORS = {
|
|
10
|
+
gold: C.GOLD_TEXT,
|
|
11
|
+
purple: C.TAROT_BUTTON,
|
|
12
|
+
blue: C.PLANET_BUTTON,
|
|
13
|
+
spectral: C.SPECTRAL_BUTTON,
|
|
14
|
+
default: C.GOLD_TEXT,
|
|
15
|
+
};
|
|
16
|
+
export function JamlAnalyzerFullscreen({ antes, live, enabledStreams, onEnabledStreamsChange, hidePicker = false, chunkSize = 12, className = "", }) {
|
|
17
|
+
const [internalEnabled, setInternalEnabled] = useState(enabledStreams ?? DEFAULT_ENABLED_STREAMS);
|
|
18
|
+
const effectiveEnabled = enabledStreams ?? internalEnabled;
|
|
19
|
+
const setEnabled = useCallback((next) => {
|
|
20
|
+
setInternalEnabled(next);
|
|
21
|
+
onEnabledStreamsChange?.(next);
|
|
22
|
+
}, [onEnabledStreamsChange]);
|
|
23
|
+
const scrollRef = useRef(null);
|
|
24
|
+
const sectionRefs = useRef(new Map());
|
|
25
|
+
const [currentAnte, setCurrentAnte] = useState(antes[0]?.ante ?? 1);
|
|
26
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const root = scrollRef.current;
|
|
29
|
+
if (!root)
|
|
30
|
+
return;
|
|
31
|
+
const observer = new IntersectionObserver((entries) => {
|
|
32
|
+
const top = entries
|
|
33
|
+
.filter((e) => e.isIntersecting)
|
|
34
|
+
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
|
35
|
+
if (!top)
|
|
36
|
+
return;
|
|
37
|
+
const ante = Number(top.target.dataset.ante);
|
|
38
|
+
if (!Number.isNaN(ante))
|
|
39
|
+
setCurrentAnte(ante);
|
|
40
|
+
}, { root, threshold: [0.4, 0.6, 0.8] });
|
|
41
|
+
sectionRefs.current.forEach((el) => observer.observe(el));
|
|
42
|
+
return () => observer.disconnect();
|
|
43
|
+
}, [antes]);
|
|
44
|
+
const scrollToAnte = useCallback((ante) => {
|
|
45
|
+
sectionRefs.current.get(ante)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
46
|
+
}, []);
|
|
47
|
+
return (_jsxs("div", { className: className, style: styles.root, children: [_jsx("div", { ref: scrollRef, style: styles.scroller, children: antes.map((ante) => (_jsx(AnteSection, { ante: ante, live: live, enabledStreams: effectiveEnabled, chunkSize: chunkSize, registerRef: (el) => {
|
|
48
|
+
if (el)
|
|
49
|
+
sectionRefs.current.set(ante.ante, el);
|
|
50
|
+
else
|
|
51
|
+
sectionRefs.current.delete(ante.ante);
|
|
52
|
+
} }, ante.ante))) }), _jsx(SideRail, { antes: antes.map((a) => a.ante), currentAnte: currentAnte, onJump: scrollToAnte }), !hidePicker && (_jsx(StreamPicker, { enabled: effectiveEnabled, onChange: setEnabled, open: pickerOpen, onToggle: () => setPickerOpen((v) => !v) }))] }));
|
|
53
|
+
}
|
|
54
|
+
function AnteSection({ ante, live, enabledStreams, chunkSize, registerRef }) {
|
|
55
|
+
return (_jsxs("section", { ref: registerRef, "data-ante": ante.ante, style: styles.section, children: [_jsxs("header", { style: styles.header, children: [_jsxs("div", { children: [_jsx("div", { style: styles.anteLabel, children: "ANTE" }), _jsx("div", { style: styles.anteNumber, children: ante.ante })] }), ante.voucher && (_jsxs("div", { style: styles.voucherBlock, children: [_jsx(JamlVoucher, { voucherName: ante.voucher, scale: 0.85 }), _jsx("div", { style: styles.voucherCaption, children: ante.voucher })] }))] }), _jsxs("div", { style: styles.blindRow, children: [_jsx(BlindCell, { label: "SMALL", tag: ante.smallBlindTag }), _jsx(BlindCell, { label: "BIG", tag: ante.bigBlindTag }), ante.boss && (_jsxs("div", { style: styles.bossCell, children: [_jsx("div", { style: styles.cellLabel, children: "BOSS" }), _jsx(JamlBoss, { bossName: ante.boss, scale: 0.7 }), _jsx("div", { style: styles.cellCaption, children: ante.boss })] }))] }), ante.packs && ante.packs.length > 0 && (_jsxs("div", { style: styles.streamLane, children: [_jsx("div", { style: styles.streamLabel, children: "PACKS" }), _jsx("div", { style: styles.packRow, children: ante.packs.map((pack, i) => (_jsx("div", { style: styles.packPill, children: pack }, `${ante.ante}-pack-${i}`))) })] })), enabledStreams.map((key) => {
|
|
56
|
+
const isShop = key === "shop";
|
|
57
|
+
const initialItems = isShop
|
|
58
|
+
? (ante.shop ?? []).map((item) => ({
|
|
59
|
+
id: item.id,
|
|
60
|
+
name: item.name,
|
|
61
|
+
value: item.value,
|
|
62
|
+
}))
|
|
63
|
+
: [];
|
|
64
|
+
return (_jsx(StreamLane, { ante: ante.ante, streamKey: key, live: live, chunkSize: chunkSize, initialItems: initialItems }, `${ante.ante}-${key}`));
|
|
65
|
+
})] }));
|
|
66
|
+
}
|
|
67
|
+
function StreamLane({ ante, streamKey, live, chunkSize, initialItems }) {
|
|
68
|
+
const meta = ANALYZER_STREAM_META[streamKey];
|
|
69
|
+
const handle = useMemo(() => (live ? buildStreamHandle(live, ante, streamKey) : null), [live, ante, streamKey]);
|
|
70
|
+
const stream = useMotelyStream(handle?.initStream ?? null, handle?.nextItem ?? null, [ante, streamKey, live?.seed, live?.deck, live?.stake], initialItems);
|
|
71
|
+
const desired = live?.desiredNames ?? new Set();
|
|
72
|
+
const toneColor = TONE_COLORS[meta.tone] ?? TONE_COLORS.default;
|
|
73
|
+
return (_jsxs("div", { style: styles.streamLane, children: [_jsxs("div", { style: { ...styles.streamLabel, color: toneColor }, children: [meta.label.toUpperCase(), stream.items.length > 0 ? ` · ${stream.items.length}` : ""] }), _jsx(ShopRow, { items: stream.items, desired: desired, loadingMore: stream.loadingMore, ready: stream.ready, onPullMore: () => stream.pullMore(chunkSize) }), stream.error && _jsxs("div", { style: styles.errorLine, children: ["stream error: ", stream.error] })] }));
|
|
74
|
+
}
|
|
75
|
+
function BlindCell({ label, tag }) {
|
|
76
|
+
if (!tag)
|
|
77
|
+
return null;
|
|
78
|
+
return (_jsxs("div", { style: styles.blindCell, children: [_jsx("div", { style: styles.cellLabel, children: label }), _jsx(JamlTag, { tagName: tag, scale: 0.7 }), _jsx("div", { style: styles.cellCaption, children: tag })] }));
|
|
79
|
+
}
|
|
80
|
+
function ShopRow({ items, desired, loadingMore, ready, onPullMore }) {
|
|
81
|
+
const sentinelRef = useRef(null);
|
|
82
|
+
const lastTriggerRef = useRef(0);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const el = sentinelRef.current;
|
|
85
|
+
if (!el || !ready)
|
|
86
|
+
return;
|
|
87
|
+
const observer = new IntersectionObserver((entries) => {
|
|
88
|
+
if (entries[0]?.isIntersecting && !loadingMore) {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
if (now - lastTriggerRef.current < 200)
|
|
91
|
+
return;
|
|
92
|
+
lastTriggerRef.current = now;
|
|
93
|
+
onPullMore();
|
|
94
|
+
}
|
|
95
|
+
}, { root: el.parentElement, threshold: 0.1, rootMargin: "0px 200px 0px 0px" });
|
|
96
|
+
observer.observe(el);
|
|
97
|
+
return () => observer.disconnect();
|
|
98
|
+
}, [ready, loadingMore, onPullMore]);
|
|
99
|
+
return (_jsxs("div", { style: styles.shopRow, children: [items.map((item) => (_jsx(ShopItem, { item: item, desired: desired.has(item.name.toLowerCase()) }, item.id))), _jsx("div", { ref: sentinelRef, style: styles.sentinel, children: loadingMore ? "…" : "" })] }));
|
|
100
|
+
}
|
|
101
|
+
function ShopItem({ item, desired }) {
|
|
102
|
+
const resolved = resolveAnalyzerShopItem({ id: item.id, name: item.name, value: item.value });
|
|
103
|
+
const wrapperStyle = {
|
|
104
|
+
flexShrink: 0,
|
|
105
|
+
position: "relative",
|
|
106
|
+
filter: desired ? `drop-shadow(0 0 6px ${C.GOLD})` : undefined,
|
|
107
|
+
};
|
|
108
|
+
const labelStyle = {
|
|
109
|
+
fontSize: 10,
|
|
110
|
+
color: desired ? C.GOLD : C.GREY,
|
|
111
|
+
textAlign: "center",
|
|
112
|
+
marginTop: 2,
|
|
113
|
+
maxWidth: 80,
|
|
114
|
+
overflow: "hidden",
|
|
115
|
+
textOverflow: "ellipsis",
|
|
116
|
+
whiteSpace: "nowrap",
|
|
117
|
+
};
|
|
118
|
+
return (_jsxs("div", { style: wrapperStyle, children: [resolved.kind === "joker" || resolved.kind === "consumable" || resolved.kind === "playing" ? (_jsx(JamlGameCard, { card: resolved.card, type: resolved.type })) : resolved.kind === "voucher" ? (_jsx(JamlVoucher, { voucherName: resolved.voucherName, scale: 0.7 })) : (_jsx("div", { style: styles.unknownTile, children: ("label" in resolved ? resolved.label : item.name) || "?" })), _jsx("div", { style: labelStyle, children: item.name })] }));
|
|
119
|
+
}
|
|
120
|
+
function SideRail({ antes, currentAnte, onJump }) {
|
|
121
|
+
return (_jsx("div", { style: styles.sideRail, children: antes.map((ante) => {
|
|
122
|
+
const active = ante === currentAnte;
|
|
123
|
+
return (_jsx("button", { type: "button", onClick: () => onJump(ante), "aria-label": `Jump to ante ${ante}`, style: {
|
|
124
|
+
...styles.sideRailDot,
|
|
125
|
+
background: active ? C.GOLD : withAlpha(C.WHITE, 0.25),
|
|
126
|
+
transform: active ? "scale(1.4)" : "scale(1)",
|
|
127
|
+
boxShadow: active ? `0 0 8px ${C.GOLD}` : "none",
|
|
128
|
+
} }, ante));
|
|
129
|
+
}) }));
|
|
130
|
+
}
|
|
131
|
+
function StreamPicker({ enabled, onChange, open, onToggle }) {
|
|
132
|
+
const enabledSet = new Set(enabled);
|
|
133
|
+
const all = Object.values(ANALYZER_STREAM_META);
|
|
134
|
+
function toggle(key) {
|
|
135
|
+
const next = new Set(enabledSet);
|
|
136
|
+
if (next.has(key))
|
|
137
|
+
next.delete(key);
|
|
138
|
+
else
|
|
139
|
+
next.add(key);
|
|
140
|
+
onChange(all.map((m) => m.key).filter((k) => next.has(k)));
|
|
141
|
+
}
|
|
142
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: onToggle, style: styles.pickerButton, "aria-label": "Toggle stream picker", children: open ? "✕" : "≡" }), open && (_jsxs("div", { style: styles.pickerPanel, children: [_jsx("div", { style: styles.pickerHeader, children: "STREAMS" }), all.map((meta) => {
|
|
143
|
+
const isOn = enabledSet.has(meta.key);
|
|
144
|
+
const tone = TONE_COLORS[meta.tone] ?? TONE_COLORS.default;
|
|
145
|
+
return (_jsxs("button", { type: "button", onClick: () => toggle(meta.key), style: {
|
|
146
|
+
...styles.pickerChip,
|
|
147
|
+
borderColor: isOn ? tone : withAlpha(C.WHITE, 0.15),
|
|
148
|
+
color: isOn ? tone : C.GREY,
|
|
149
|
+
background: isOn ? withAlpha(tone, 0.1) : "transparent",
|
|
150
|
+
}, children: [isOn ? "●" : "○", " ", meta.label] }, meta.key));
|
|
151
|
+
})] }))] }));
|
|
152
|
+
}
|
|
153
|
+
const styles = {
|
|
154
|
+
root: {
|
|
155
|
+
position: "relative",
|
|
156
|
+
width: "100%",
|
|
157
|
+
height: "100svh",
|
|
158
|
+
background: C.DARKEST,
|
|
159
|
+
color: C.WHITE,
|
|
160
|
+
fontFamily: "var(--font-sans, m6x11plus), monospace",
|
|
161
|
+
overflow: "hidden",
|
|
162
|
+
},
|
|
163
|
+
scroller: {
|
|
164
|
+
width: "100%",
|
|
165
|
+
height: "100%",
|
|
166
|
+
overflowY: "scroll",
|
|
167
|
+
scrollSnapType: "y mandatory",
|
|
168
|
+
scrollBehavior: "smooth",
|
|
169
|
+
WebkitOverflowScrolling: "touch",
|
|
170
|
+
},
|
|
171
|
+
section: {
|
|
172
|
+
width: "100%",
|
|
173
|
+
minHeight: "100svh",
|
|
174
|
+
scrollSnapAlign: "start",
|
|
175
|
+
padding: "20px 16px 28px",
|
|
176
|
+
display: "flex",
|
|
177
|
+
flexDirection: "column",
|
|
178
|
+
gap: 12,
|
|
179
|
+
boxSizing: "border-box",
|
|
180
|
+
borderBottom: `1px solid ${withAlpha(C.WHITE, 0.05)}`,
|
|
181
|
+
},
|
|
182
|
+
header: {
|
|
183
|
+
display: "flex",
|
|
184
|
+
justifyContent: "space-between",
|
|
185
|
+
alignItems: "flex-start",
|
|
186
|
+
gap: 12,
|
|
187
|
+
},
|
|
188
|
+
anteLabel: {
|
|
189
|
+
fontSize: 12,
|
|
190
|
+
color: C.GREY,
|
|
191
|
+
letterSpacing: "0.16em",
|
|
192
|
+
},
|
|
193
|
+
anteNumber: {
|
|
194
|
+
fontSize: 72,
|
|
195
|
+
color: C.GOLD,
|
|
196
|
+
lineHeight: 0.9,
|
|
197
|
+
textShadow: `3px 3px 0 ${C.BLACK}`,
|
|
198
|
+
},
|
|
199
|
+
voucherBlock: {
|
|
200
|
+
display: "flex",
|
|
201
|
+
flexDirection: "column",
|
|
202
|
+
alignItems: "center",
|
|
203
|
+
gap: 4,
|
|
204
|
+
},
|
|
205
|
+
voucherCaption: {
|
|
206
|
+
fontSize: 10,
|
|
207
|
+
color: C.PURPLE,
|
|
208
|
+
letterSpacing: "0.1em",
|
|
209
|
+
},
|
|
210
|
+
blindRow: {
|
|
211
|
+
display: "flex",
|
|
212
|
+
gap: 16,
|
|
213
|
+
flexWrap: "wrap",
|
|
214
|
+
},
|
|
215
|
+
blindCell: {
|
|
216
|
+
display: "flex",
|
|
217
|
+
flexDirection: "column",
|
|
218
|
+
alignItems: "center",
|
|
219
|
+
gap: 4,
|
|
220
|
+
},
|
|
221
|
+
bossCell: {
|
|
222
|
+
display: "flex",
|
|
223
|
+
flexDirection: "column",
|
|
224
|
+
alignItems: "center",
|
|
225
|
+
gap: 4,
|
|
226
|
+
},
|
|
227
|
+
cellLabel: {
|
|
228
|
+
fontSize: 10,
|
|
229
|
+
color: C.GREY,
|
|
230
|
+
letterSpacing: "0.12em",
|
|
231
|
+
},
|
|
232
|
+
cellCaption: {
|
|
233
|
+
fontSize: 10,
|
|
234
|
+
color: C.WHITE,
|
|
235
|
+
maxWidth: 90,
|
|
236
|
+
textAlign: "center",
|
|
237
|
+
overflow: "hidden",
|
|
238
|
+
textOverflow: "ellipsis",
|
|
239
|
+
whiteSpace: "nowrap",
|
|
240
|
+
},
|
|
241
|
+
streamLane: {
|
|
242
|
+
display: "flex",
|
|
243
|
+
flexDirection: "column",
|
|
244
|
+
gap: 6,
|
|
245
|
+
minHeight: 0,
|
|
246
|
+
},
|
|
247
|
+
streamLabel: {
|
|
248
|
+
fontSize: 11,
|
|
249
|
+
color: C.GOLD_TEXT,
|
|
250
|
+
letterSpacing: "0.16em",
|
|
251
|
+
},
|
|
252
|
+
shopRow: {
|
|
253
|
+
display: "flex",
|
|
254
|
+
gap: 10,
|
|
255
|
+
overflowX: "auto",
|
|
256
|
+
overflowY: "hidden",
|
|
257
|
+
paddingBottom: 6,
|
|
258
|
+
scrollbarWidth: "thin",
|
|
259
|
+
},
|
|
260
|
+
packRow: {
|
|
261
|
+
display: "flex",
|
|
262
|
+
flexWrap: "wrap",
|
|
263
|
+
gap: 6,
|
|
264
|
+
},
|
|
265
|
+
packPill: {
|
|
266
|
+
padding: "4px 10px",
|
|
267
|
+
background: withAlpha(C.PANEL_EDGE, 0.9),
|
|
268
|
+
border: `1px solid ${C.INNER_BORDER}`,
|
|
269
|
+
borderRadius: 4,
|
|
270
|
+
fontSize: 11,
|
|
271
|
+
color: C.WHITE,
|
|
272
|
+
},
|
|
273
|
+
sentinel: {
|
|
274
|
+
flexShrink: 0,
|
|
275
|
+
width: 28,
|
|
276
|
+
display: "flex",
|
|
277
|
+
alignItems: "center",
|
|
278
|
+
justifyContent: "center",
|
|
279
|
+
color: C.GREY,
|
|
280
|
+
fontSize: 14,
|
|
281
|
+
},
|
|
282
|
+
unknownTile: {
|
|
283
|
+
width: 71,
|
|
284
|
+
height: 95,
|
|
285
|
+
border: `1px dashed ${C.GREY}`,
|
|
286
|
+
borderRadius: 4,
|
|
287
|
+
display: "flex",
|
|
288
|
+
alignItems: "center",
|
|
289
|
+
justifyContent: "center",
|
|
290
|
+
fontSize: 9,
|
|
291
|
+
color: C.GREY,
|
|
292
|
+
padding: 4,
|
|
293
|
+
textAlign: "center",
|
|
294
|
+
},
|
|
295
|
+
errorLine: {
|
|
296
|
+
fontSize: 10,
|
|
297
|
+
color: C.RED,
|
|
298
|
+
},
|
|
299
|
+
sideRail: {
|
|
300
|
+
position: "absolute",
|
|
301
|
+
top: "50%",
|
|
302
|
+
right: 6,
|
|
303
|
+
transform: "translateY(-50%)",
|
|
304
|
+
display: "flex",
|
|
305
|
+
flexDirection: "column",
|
|
306
|
+
gap: 8,
|
|
307
|
+
zIndex: 5,
|
|
308
|
+
pointerEvents: "auto",
|
|
309
|
+
},
|
|
310
|
+
sideRailDot: {
|
|
311
|
+
width: 8,
|
|
312
|
+
height: 8,
|
|
313
|
+
borderRadius: "50%",
|
|
314
|
+
border: "none",
|
|
315
|
+
cursor: "pointer",
|
|
316
|
+
padding: 0,
|
|
317
|
+
transition: "transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease",
|
|
318
|
+
},
|
|
319
|
+
pickerButton: {
|
|
320
|
+
position: "absolute",
|
|
321
|
+
top: 12,
|
|
322
|
+
right: 12,
|
|
323
|
+
width: 32,
|
|
324
|
+
height: 32,
|
|
325
|
+
borderRadius: 4,
|
|
326
|
+
border: `1px solid ${withAlpha(C.WHITE, 0.2)}`,
|
|
327
|
+
background: withAlpha(C.DARK_GREY, 0.85),
|
|
328
|
+
color: C.WHITE,
|
|
329
|
+
fontSize: 16,
|
|
330
|
+
cursor: "pointer",
|
|
331
|
+
zIndex: 6,
|
|
332
|
+
fontFamily: "inherit",
|
|
333
|
+
},
|
|
334
|
+
pickerPanel: {
|
|
335
|
+
position: "absolute",
|
|
336
|
+
top: 50,
|
|
337
|
+
right: 12,
|
|
338
|
+
width: 220,
|
|
339
|
+
maxHeight: "70vh",
|
|
340
|
+
overflowY: "auto",
|
|
341
|
+
padding: 10,
|
|
342
|
+
background: withAlpha(C.DARK_GREY, 0.95),
|
|
343
|
+
border: `1px solid ${withAlpha(C.WHITE, 0.15)}`,
|
|
344
|
+
borderRadius: 6,
|
|
345
|
+
display: "flex",
|
|
346
|
+
flexDirection: "column",
|
|
347
|
+
gap: 4,
|
|
348
|
+
zIndex: 6,
|
|
349
|
+
backdropFilter: "blur(4px)",
|
|
350
|
+
},
|
|
351
|
+
pickerHeader: {
|
|
352
|
+
fontSize: 10,
|
|
353
|
+
color: C.GREY,
|
|
354
|
+
letterSpacing: "0.16em",
|
|
355
|
+
marginBottom: 4,
|
|
356
|
+
},
|
|
357
|
+
pickerChip: {
|
|
358
|
+
padding: "6px 10px",
|
|
359
|
+
border: "1px solid",
|
|
360
|
+
borderRadius: 4,
|
|
361
|
+
fontSize: 11,
|
|
362
|
+
fontFamily: "inherit",
|
|
363
|
+
cursor: "pointer",
|
|
364
|
+
textAlign: "left",
|
|
365
|
+
transition: "all 0.12s ease",
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
export { ANALYZER_STREAM_META } from "../hooks/analyzerStreamRegistry.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
3
4
|
import Editor from "@monaco-editor/react";
|
|
4
5
|
import { JimboColorOption } from "../ui/tokens.js";
|
|
5
6
|
// Monaco needs hex strings for its colors API. We strip the leading `#` from
|
|
@@ -40,10 +41,77 @@ const defineBalatroTheme = (monaco) => {
|
|
|
40
41
|
});
|
|
41
42
|
};
|
|
42
43
|
export function JamlCodeEditor({ value, onChange, minHeight = 320, }) {
|
|
44
|
+
const editorRef = useRef(null);
|
|
45
|
+
const monacoRef = useRef(null);
|
|
46
|
+
// Suppress our onChange while we're applying a programmatic edit, so the
|
|
47
|
+
// streamed parent value doesn't loop back through onChange and bounce.
|
|
48
|
+
const suppressEmitRef = useRef(false);
|
|
49
|
+
// Capture initial value for the uncontrolled editor mount; subsequent
|
|
50
|
+
// updates flow through the useEffect below.
|
|
51
|
+
const initialValueRef = useRef(value);
|
|
52
|
+
// Track value across renders so we can apply only the streamed delta when
|
|
53
|
+
// the new value is a strict suffix-extension of what's already in the model.
|
|
54
|
+
const lastSyncedValueRef = useRef(value);
|
|
55
|
+
const handleMount = (editor, m) => {
|
|
56
|
+
editorRef.current = editor;
|
|
57
|
+
monacoRef.current = m;
|
|
58
|
+
};
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const editor = editorRef.current;
|
|
61
|
+
const m = monacoRef.current;
|
|
62
|
+
if (!editor || !m)
|
|
63
|
+
return;
|
|
64
|
+
const model = editor.getModel();
|
|
65
|
+
if (!model)
|
|
66
|
+
return;
|
|
67
|
+
const current = editor.getValue();
|
|
68
|
+
if (current === value) {
|
|
69
|
+
lastSyncedValueRef.current = value;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
suppressEmitRef.current = true;
|
|
73
|
+
try {
|
|
74
|
+
// Streaming-friendly path: when the new value just appends to what
|
|
75
|
+
// Monaco already has, push an insert at end-of-document. Monaco
|
|
76
|
+
// re-tokenizes only from the insertion point — no full-doc churn,
|
|
77
|
+
// no syntax-color strobe, no cursor reset.
|
|
78
|
+
if (value.length > current.length && value.startsWith(current)) {
|
|
79
|
+
const suffix = value.slice(current.length);
|
|
80
|
+
const lastLine = model.getLineCount();
|
|
81
|
+
const lastCol = model.getLineMaxColumn(lastLine);
|
|
82
|
+
model.applyEdits([
|
|
83
|
+
{
|
|
84
|
+
range: new m.Range(lastLine, lastCol, lastLine, lastCol),
|
|
85
|
+
text: suffix,
|
|
86
|
+
forceMoveMarkers: false,
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Real replacement (paste from outside, parent rewrite, undo, etc.).
|
|
92
|
+
editor.executeEdits("", [
|
|
93
|
+
{
|
|
94
|
+
range: model.getFullModelRange(),
|
|
95
|
+
text: value,
|
|
96
|
+
forceMoveMarkers: true,
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
editor.pushUndoStop();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
suppressEmitRef.current = false;
|
|
104
|
+
lastSyncedValueRef.current = value;
|
|
105
|
+
}
|
|
106
|
+
}, [value]);
|
|
43
107
|
return (_jsxs("div", { style: { width: "100%", minHeight, background: JimboColorOption.DARKEST }, children: [_jsx("style", { children: `
|
|
44
108
|
.monaco-editor .iPadShowKeyboard,
|
|
45
109
|
.monaco-editor [class*="iPadShowKeyboard"] { display: none !important; }
|
|
46
|
-
` }), _jsx(Editor, { height: `${minHeight}px`, defaultLanguage: "yaml",
|
|
110
|
+
` }), _jsx(Editor, { height: `${minHeight}px`, defaultLanguage: "yaml", defaultValue: initialValueRef.current, theme: "jaml-balatro-dark", onChange: (next) => {
|
|
111
|
+
if (suppressEmitRef.current)
|
|
112
|
+
return;
|
|
113
|
+
onChange(next ?? "");
|
|
114
|
+
}, onMount: handleMount, beforeMount: defineBalatroTheme, options: {
|
|
47
115
|
minimap: { enabled: false },
|
|
48
116
|
scrollBeyondLastLine: false,
|
|
49
117
|
fontSize: 13,
|
|
@@ -111,8 +111,10 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
|
|
|
111
111
|
// doesn't flash the visual panel empty.
|
|
112
112
|
const [lastParsedText, setLastParsedText] = useState("");
|
|
113
113
|
const [lastParsedFilter, setLastParsedFilter] = useState(() => jamlTextToVisualFilter(jaml ?? defaultJaml ?? ""));
|
|
114
|
-
// Adjust-state-during-render: reparse when text changes (only if not
|
|
115
|
-
|
|
114
|
+
// Adjust-state-during-render: reparse when text changes (only if not
|
|
115
|
+
// controlled). Gated on `mode === "visual"` so we don't burn CPU parsing
|
|
116
|
+
// on every streamed token while the user is in the .jaml/map/results tab.
|
|
117
|
+
if (visualFilter === undefined && mode === "visual" && text !== lastParsedText) {
|
|
116
118
|
try {
|
|
117
119
|
const parsed = jamlTextToVisualFilter(text);
|
|
118
120
|
setLastParsedText(text);
|
|
@@ -1,62 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { JimboButton } from "../ui/panel.js";
|
|
4
|
+
import { JimboTabs } from "../ui/jimboTabs.js";
|
|
4
5
|
import { JimboColorOption } from "../ui/tokens.js";
|
|
5
|
-
const TABS = [
|
|
6
|
-
{ id: "visual", label: "Visual" },
|
|
7
|
-
{ id: "code", label: ".jaml" },
|
|
8
|
-
{ id: "map", label: "Map" },
|
|
9
|
-
{ id: "results", label: "Results" },
|
|
10
|
-
];
|
|
11
6
|
export function JamlIdeToolbar({ mode, onModeChange, resultCount = 0, className = "", onSearch, isSearching = false }) {
|
|
7
|
+
const tabs = [
|
|
8
|
+
{ id: "visual", label: "Visual" },
|
|
9
|
+
{ id: "code", label: ".jaml" },
|
|
10
|
+
{ id: "map", label: "Map" },
|
|
11
|
+
{ id: "results", label: resultCount > 0 ? `Results (${resultCount})` : "Results" },
|
|
12
|
+
];
|
|
12
13
|
return (_jsxs("div", { className: className, style: {
|
|
13
14
|
display: "flex",
|
|
14
15
|
alignItems: "center",
|
|
15
16
|
justifyContent: "space-between",
|
|
16
17
|
gap: 8,
|
|
17
|
-
padding: "
|
|
18
|
+
padding: "10px 10px 6px",
|
|
18
19
|
borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
|
|
19
20
|
background: JimboColorOption.DARKEST,
|
|
20
|
-
}, children: [_jsx("
|
|
21
|
-
display: "flex",
|
|
22
|
-
alignItems: "stretch",
|
|
23
|
-
gap: 0,
|
|
24
|
-
background: `${JimboColorOption.DARK_GREY}cc`,
|
|
25
|
-
borderRadius: 7,
|
|
26
|
-
border: `1px solid ${JimboColorOption.PANEL_EDGE}`,
|
|
27
|
-
padding: 2,
|
|
28
|
-
overflow: "hidden",
|
|
29
|
-
}, children: TABS.map((tab) => {
|
|
30
|
-
const isActive = mode === tab.id;
|
|
31
|
-
return (_jsxs("button", { type: "button", onClick: () => onModeChange(tab.id), style: {
|
|
32
|
-
display: "flex",
|
|
33
|
-
alignItems: "center",
|
|
34
|
-
gap: 5,
|
|
35
|
-
padding: "3px 10px",
|
|
36
|
-
border: "none",
|
|
37
|
-
borderRadius: 5,
|
|
38
|
-
cursor: "pointer",
|
|
39
|
-
fontFamily: "m6x11plus, monospace",
|
|
40
|
-
fontSize: 10,
|
|
41
|
-
letterSpacing: 1,
|
|
42
|
-
textTransform: "uppercase",
|
|
43
|
-
lineHeight: 1.2,
|
|
44
|
-
transition: "background 80ms, color 80ms, box-shadow 80ms",
|
|
45
|
-
color: isActive ? JimboColorOption.DARKEST : JimboColorOption.GREY,
|
|
46
|
-
background: isActive ? JimboColorOption.GOLD : "transparent",
|
|
47
|
-
boxShadow: isActive ? `0 2px 0 #8a6a1e` : "none",
|
|
48
|
-
textShadow: isActive ? "none" : "none",
|
|
49
|
-
userSelect: "none",
|
|
50
|
-
position: "relative",
|
|
51
|
-
transform: isActive ? "translateY(0)" : "translateY(0)",
|
|
52
|
-
}, children: [tab.label, tab.id === "results" && resultCount > 0 ? (_jsx("span", { style: {
|
|
53
|
-
borderRadius: 999,
|
|
54
|
-
background: isActive ? `${JimboColorOption.DARKEST}44` : `${JimboColorOption.GOLD}33`,
|
|
55
|
-
color: isActive ? JimboColorOption.DARKEST : JimboColorOption.GOLD_TEXT,
|
|
56
|
-
padding: "0px 5px",
|
|
57
|
-
fontSize: 9,
|
|
58
|
-
fontFamily: "monospace",
|
|
59
|
-
letterSpacing: 0,
|
|
60
|
-
}, children: resultCount })) : null] }, tab.id));
|
|
61
|
-
}) }), onSearch ? (_jsx(JimboButton, { tone: isSearching ? "red" : "blue", size: "xs", onClick: onSearch, children: isSearching ? "Stop" : "Search" })) : null] }));
|
|
21
|
+
}, children: [_jsx(JimboTabs, { tabs: tabs, activeTab: mode, onTabChange: (id) => onModeChange(id) }), onSearch ? (_jsx(JimboButton, { tone: isSearching ? "red" : "blue", size: "xs", onClick: onSearch, children: isSearching ? "Stop" : "Search" })) : null] }));
|
|
62
22
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { StreamItem } from "./useShopStream.js";
|
|
2
|
+
import type { AnalyzerLive } from "./useAnalyzer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registry of motely-wasm streams the fullscreen analyzer can surface as
|
|
5
|
+
* lanes. Each entry knows how to (a) open a stream against a live ctx +
|
|
6
|
+
* runState for a given ante, and (b) pull the next item.
|
|
7
|
+
*
|
|
8
|
+
* Streams that produce raw item *values* (jokers, tarots, planets,
|
|
9
|
+
* spectrals) are normalized through `motelyItemDisplayNameFromValue` so
|
|
10
|
+
* downstream rendering uses one shared resolver. Pack-contents streams
|
|
11
|
+
* return arrays in a single call; we flatten to per-card items keyed by
|
|
12
|
+
* pack-pull index so React keys stay stable across reloads.
|
|
13
|
+
*/
|
|
14
|
+
export type AnalyzerStreamKey = "shop" | "soulJoker" | "rareTagJoker" | "uncommonTagJoker" | "riffRaffJoker" | "buffoonJoker" | "judgementJoker" | "wraithJoker" | "shopJoker" | "shopTarot" | "shopPlanet" | "shopSpectral" | "purpleSealTarot";
|
|
15
|
+
export interface AnalyzerStreamMeta {
|
|
16
|
+
key: AnalyzerStreamKey;
|
|
17
|
+
label: string;
|
|
18
|
+
/** Tone hint for the lane header. */
|
|
19
|
+
tone: "gold" | "purple" | "blue" | "spectral" | "default";
|
|
20
|
+
/** Whether this stream is on by default in the picker. */
|
|
21
|
+
defaultEnabled: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare const ANALYZER_STREAM_META: Record<AnalyzerStreamKey, AnalyzerStreamMeta>;
|
|
24
|
+
export declare const DEFAULT_ENABLED_STREAMS: AnalyzerStreamKey[];
|
|
25
|
+
interface StreamHandle {
|
|
26
|
+
initStream: () => void;
|
|
27
|
+
nextItem: () => StreamItem;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build init/next callbacks for a (key, ante) pair against the live ctx.
|
|
31
|
+
* The component wraps these with useMotelyStream. Items are normalized to
|
|
32
|
+
* { id, name, value } so the same ShopItem renderer can show every lane.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildStreamHandle(live: AnalyzerLive, ante: number, key: AnalyzerStreamKey): StreamHandle | null;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import { motelyItemDisplayNameFromValue } from "../motelyDisplay.js";
|
|
4
|
+
export const ANALYZER_STREAM_META = {
|
|
5
|
+
shop: { key: "shop", label: "Shop", tone: "default", defaultEnabled: true },
|
|
6
|
+
shopJoker: { key: "shopJoker", label: "Shop Jokers", tone: "default", defaultEnabled: false },
|
|
7
|
+
soulJoker: { key: "soulJoker", label: "Soul (Legendary)", tone: "gold", defaultEnabled: true },
|
|
8
|
+
rareTagJoker: { key: "rareTagJoker", label: "Rare Tag Jokers", tone: "default", defaultEnabled: false },
|
|
9
|
+
uncommonTagJoker: { key: "uncommonTagJoker", label: "Uncommon Tag Jokers", tone: "default", defaultEnabled: false },
|
|
10
|
+
riffRaffJoker: { key: "riffRaffJoker", label: "Riff Raff", tone: "default", defaultEnabled: false },
|
|
11
|
+
buffoonJoker: { key: "buffoonJoker", label: "Buffoon Pack Jokers", tone: "default", defaultEnabled: false },
|
|
12
|
+
judgementJoker: { key: "judgementJoker", label: "Judgement Jokers", tone: "purple", defaultEnabled: false },
|
|
13
|
+
wraithJoker: { key: "wraithJoker", label: "Wraith Jokers", tone: "spectral", defaultEnabled: false },
|
|
14
|
+
shopTarot: { key: "shopTarot", label: "Shop Tarots", tone: "purple", defaultEnabled: false },
|
|
15
|
+
shopPlanet: { key: "shopPlanet", label: "Shop Planets", tone: "blue", defaultEnabled: false },
|
|
16
|
+
shopSpectral: { key: "shopSpectral", label: "Shop Spectrals", tone: "spectral", defaultEnabled: false },
|
|
17
|
+
purpleSealTarot: { key: "purpleSealTarot", label: "Purple Seal Tarots", tone: "purple", defaultEnabled: false },
|
|
18
|
+
};
|
|
19
|
+
export const DEFAULT_ENABLED_STREAMS = (Object.values(ANALYZER_STREAM_META)
|
|
20
|
+
.filter((m) => m.defaultEnabled)
|
|
21
|
+
.map((m) => m.key));
|
|
22
|
+
/**
|
|
23
|
+
* Build init/next callbacks for a (key, ante) pair against the live ctx.
|
|
24
|
+
* The component wraps these with useMotelyStream. Items are normalized to
|
|
25
|
+
* { id, name, value } so the same ShopItem renderer can show every lane.
|
|
26
|
+
*/
|
|
27
|
+
export function buildStreamHandle(live, ante, key) {
|
|
28
|
+
const ctx = live.ctx;
|
|
29
|
+
const Motely = live.Motely;
|
|
30
|
+
const runState = live.runStates[ante];
|
|
31
|
+
let stream = null;
|
|
32
|
+
let cursor = 0;
|
|
33
|
+
const idBase = `${ante}-${key}`;
|
|
34
|
+
function nameFromValue(value) {
|
|
35
|
+
return motelyItemDisplayNameFromValue(value);
|
|
36
|
+
}
|
|
37
|
+
function joker(streamFactoryName, nextName) {
|
|
38
|
+
return {
|
|
39
|
+
initStream: () => {
|
|
40
|
+
stream = ctx[streamFactoryName](ante, Motely.MotelyJokerStreamFlags.Default);
|
|
41
|
+
cursor = 0;
|
|
42
|
+
},
|
|
43
|
+
nextItem: () => {
|
|
44
|
+
const r = ctx[nextName](stream);
|
|
45
|
+
const value = r.joker?.value ?? r.item?.value ?? 0;
|
|
46
|
+
const id = `${idBase}-${cursor++}`;
|
|
47
|
+
return { id, name: nameFromValue(value), value };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function fixedRarityJoker(streamFactoryName, nextName) {
|
|
52
|
+
return {
|
|
53
|
+
initStream: () => {
|
|
54
|
+
stream = ctx[streamFactoryName](ante, Motely.MotelyJokerFixedRarityStreamFlags.Default);
|
|
55
|
+
cursor = 0;
|
|
56
|
+
},
|
|
57
|
+
nextItem: () => {
|
|
58
|
+
const r = ctx[nextName](stream);
|
|
59
|
+
const value = r.joker?.value ?? 0;
|
|
60
|
+
const id = `${idBase}-${cursor++}`;
|
|
61
|
+
return { id, name: nameFromValue(value), value };
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function tarot(streamFactoryName, nextName) {
|
|
66
|
+
return {
|
|
67
|
+
initStream: () => {
|
|
68
|
+
stream = ctx[streamFactoryName](ante);
|
|
69
|
+
cursor = 0;
|
|
70
|
+
},
|
|
71
|
+
nextItem: () => {
|
|
72
|
+
const r = ctx[nextName](stream);
|
|
73
|
+
const value = r.tarot?.value ?? r.item?.value ?? 0;
|
|
74
|
+
const id = `${idBase}-${cursor++}`;
|
|
75
|
+
return { id, name: nameFromValue(value), value };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function planet(streamFactoryName, nextName) {
|
|
80
|
+
return {
|
|
81
|
+
initStream: () => {
|
|
82
|
+
stream = ctx[streamFactoryName](ante);
|
|
83
|
+
cursor = 0;
|
|
84
|
+
},
|
|
85
|
+
nextItem: () => {
|
|
86
|
+
const r = ctx[nextName](stream);
|
|
87
|
+
const value = r.planet?.value ?? 0;
|
|
88
|
+
const id = `${idBase}-${cursor++}`;
|
|
89
|
+
return { id, name: nameFromValue(value), value };
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function spectral(streamFactoryName, nextName) {
|
|
94
|
+
return {
|
|
95
|
+
initStream: () => {
|
|
96
|
+
stream = ctx[streamFactoryName](ante);
|
|
97
|
+
cursor = 0;
|
|
98
|
+
},
|
|
99
|
+
nextItem: () => {
|
|
100
|
+
const r = ctx[nextName](stream);
|
|
101
|
+
const value = r.spectral?.value ?? 0;
|
|
102
|
+
const id = `${idBase}-${cursor++}`;
|
|
103
|
+
return { id, name: nameFromValue(value), value };
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
switch (key) {
|
|
108
|
+
case "shop":
|
|
109
|
+
return {
|
|
110
|
+
initStream: () => {
|
|
111
|
+
stream = ctx.createShopItemStream(ante, runState, Motely.MotelyShopStreamFlags.Default, Motely.MotelyJokerStreamFlags.Default);
|
|
112
|
+
cursor = 0;
|
|
113
|
+
},
|
|
114
|
+
nextItem: () => {
|
|
115
|
+
const r = ctx.getNextShopItem(stream);
|
|
116
|
+
const value = r.item.value;
|
|
117
|
+
const id = `${idBase}-${cursor++}`;
|
|
118
|
+
return { id, name: nameFromValue(value), value };
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
case "shopJoker":
|
|
122
|
+
return joker("createShopJokerStream", "getNextShopJoker");
|
|
123
|
+
case "soulJoker":
|
|
124
|
+
return fixedRarityJoker("createSoulJokerStream", "getNextSoulJoker");
|
|
125
|
+
case "rareTagJoker":
|
|
126
|
+
return fixedRarityJoker("createRareTagJokerStream", "getNextRareTagJoker");
|
|
127
|
+
case "uncommonTagJoker":
|
|
128
|
+
return fixedRarityJoker("createUncommonTagJokerStream", "getNextUncommonTagJoker");
|
|
129
|
+
case "riffRaffJoker":
|
|
130
|
+
return fixedRarityJoker("createRiffRaffJokerStream", "getNextRiffRaffJoker");
|
|
131
|
+
case "buffoonJoker":
|
|
132
|
+
return joker("createBuffoonPackJokerStream", "getNextBuffoonPackJoker");
|
|
133
|
+
case "judgementJoker":
|
|
134
|
+
return joker("createJudgementJokerStream", "getNextJudgementJoker");
|
|
135
|
+
case "wraithJoker":
|
|
136
|
+
return joker("createWraithJokerStream", "getNextWraithJoker");
|
|
137
|
+
case "shopTarot":
|
|
138
|
+
return tarot("createShopTarotStream", "getNextShopTarot");
|
|
139
|
+
case "shopPlanet":
|
|
140
|
+
return planet("createShopPlanetStream", "getNextShopPlanet");
|
|
141
|
+
case "shopSpectral":
|
|
142
|
+
return spectral("createShopSpectralStream", "getNextShopSpectral");
|
|
143
|
+
case "purpleSealTarot":
|
|
144
|
+
return tarot("createPurpleSealTarotStream", "getNextPurpleSealTarot");
|
|
145
|
+
default:
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import type { AnalyzerAnteView } from "../components/AnalyzerExplorer.js";
|
|
2
2
|
export type AnalyzerStatus = "idle" | "running" | "done" | "error";
|
|
3
|
+
export type MotelyJsRunState = {
|
|
4
|
+
voucherBitfield: number;
|
|
5
|
+
bossBitfield: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Snapshot of the live search context after `analyze()` resolves, exposed so
|
|
9
|
+
* higher-level components can open additional streams (shop chunks, soul-
|
|
10
|
+
* joker chunks, pack contents) on demand without re-walking the seed. The
|
|
11
|
+
* `runStates` map is keyed by ante and holds the runState AFTER the boss +
|
|
12
|
+
* voucher have been resolved for that ante — i.e. the correct input for
|
|
13
|
+
* `createShopItemStream(ante, runState, ...)`.
|
|
14
|
+
*/
|
|
15
|
+
export interface AnalyzerLive {
|
|
16
|
+
ctx: any;
|
|
17
|
+
Motely: any;
|
|
18
|
+
runStates: Record<number, MotelyJsRunState>;
|
|
19
|
+
desiredNames: ReadonlySet<string>;
|
|
20
|
+
seed: string;
|
|
21
|
+
deck: string;
|
|
22
|
+
stake: string;
|
|
23
|
+
}
|
|
3
24
|
export declare function useAnalyzer(motelyWasmUrl: string): {
|
|
4
25
|
antes: AnalyzerAnteView[];
|
|
5
26
|
status: AnalyzerStatus;
|
|
6
27
|
error: string | null;
|
|
7
28
|
analyze: (seed: string, deck: string, stake: string, jaml?: string) => Promise<void>;
|
|
8
29
|
clearError: () => void;
|
|
30
|
+
live: AnalyzerLive | null;
|
|
9
31
|
};
|
|
@@ -7,8 +7,10 @@ export function useAnalyzer(motelyWasmUrl) {
|
|
|
7
7
|
const [antes, setAntes] = useState([]);
|
|
8
8
|
const [status, setStatus] = useState("idle");
|
|
9
9
|
const [error, setError] = useState(null);
|
|
10
|
+
const [live, setLive] = useState(null);
|
|
10
11
|
const analyze = useCallback(async (seed, deck, stake, jaml) => {
|
|
11
12
|
setAntes([]);
|
|
13
|
+
setLive(null);
|
|
12
14
|
setStatus("running");
|
|
13
15
|
setError(null);
|
|
14
16
|
try {
|
|
@@ -26,6 +28,7 @@ export function useAnalyzer(motelyWasmUrl) {
|
|
|
26
28
|
const bossStream = ctx.createBossStream();
|
|
27
29
|
let runState = { voucherBitfield: 0, bossBitfield: 0 };
|
|
28
30
|
const results = [];
|
|
31
|
+
const runStates = {};
|
|
29
32
|
for (let ante = 1; ante <= 8; ante++) {
|
|
30
33
|
const bossResult = ctx.getNextBossForAnte(bossStream, ante, runState);
|
|
31
34
|
const bossName = Motely.MotelyBossBlind[bossResult.boss] ?? `Unknown(${bossResult.boss})`;
|
|
@@ -33,6 +36,7 @@ export function useAnalyzer(motelyWasmUrl) {
|
|
|
33
36
|
const voucherResult = ctx.getAnteFirstVoucher(ante, runState);
|
|
34
37
|
const voucherName = Motely.MotelyVoucher[voucherResult.voucher] ?? `Unknown(${voucherResult.voucher})`;
|
|
35
38
|
runState = voucherResult.runState;
|
|
39
|
+
runStates[ante] = { ...runState };
|
|
36
40
|
const tagStream = ctx.createTagStream(ante);
|
|
37
41
|
const tag1 = ctx.getNextTag(tagStream);
|
|
38
42
|
const tag2 = ctx.getNextTag(tagStream);
|
|
@@ -61,6 +65,7 @@ export function useAnalyzer(motelyWasmUrl) {
|
|
|
61
65
|
});
|
|
62
66
|
}
|
|
63
67
|
setAntes(results);
|
|
68
|
+
setLive({ ctx, Motely, runStates, desiredNames, seed, deck, stake });
|
|
64
69
|
setStatus("done");
|
|
65
70
|
}
|
|
66
71
|
catch (e) {
|
|
@@ -72,5 +77,5 @@ export function useAnalyzer(motelyWasmUrl) {
|
|
|
72
77
|
setError(null);
|
|
73
78
|
setStatus((s) => (s === "error" ? "idle" : s));
|
|
74
79
|
}, []);
|
|
75
|
-
return { antes, status, error, analyze, clearError };
|
|
80
|
+
return { antes, status, error, analyze, clearError, live };
|
|
76
81
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { Layer, type LayerOptions } from "./render/Layer.js";
|
|
|
3
3
|
export { JamlCardRenderer, type JamlCardRendererProps } from "./render/CanvasRenderer.js";
|
|
4
4
|
export { JamlGameCard, JamlVoucher, JamlTag, JamlBoss, resolveAnalyzerShopItem, type JamlGameCardProps, type AnalyzerShopItem, type AnalyzerResolvedItem, } from "./components/GameCard.js";
|
|
5
5
|
export { AnalyzerExplorer, type AnalyzerAnteView, type AnalyzerBadge, type AnalyzerExplorerProps, type AnalyzerFact, type AnalyzerHighlight, type AnalyzerItem, } from "./components/AnalyzerExplorer.js";
|
|
6
|
+
export { JamlAnalyzerFullscreen, type JamlAnalyzerFullscreenProps, } from "./components/JamlAnalyzerFullscreen.js";
|
|
7
|
+
export { ANALYZER_STREAM_META, DEFAULT_ENABLED_STREAMS, type AnalyzerStreamKey, type AnalyzerStreamMeta, } from "./hooks/analyzerStreamRegistry.js";
|
|
6
8
|
export { JamlMapPreview, type JamlMapPreviewProps } from "./components/JamlMapPreview.js";
|
|
7
9
|
export { JamlIde, type JamlIdeProps, type JamlIdeSearchResult, type JamlVisualFilter, type JamlVisualClause, type JamlZone, } from "./components/JamlIde.js";
|
|
8
10
|
export { JamlIdeVisual, type JamlIdeVisualProps, } from "./components/JamlIdeVisual.js";
|
|
@@ -16,9 +18,10 @@ export { JimboTooltip, type JimboTooltipProps, type JimboTooltipMode, type Jimbo
|
|
|
16
18
|
export { JamlIdeToolbar, type JamlIdeMode, type JamlIdeToolbarProps, } from "./components/JamlIdeToolbar.js";
|
|
17
19
|
export { CardList, type CardListProps } from "./components/CardList.js";
|
|
18
20
|
export { CardFan, type CardFanProps } from "./components/CardFan.js";
|
|
21
|
+
export { RealPlayingCard, type CardSuit, type CardRank, type CardEnhancement, type CardSeal, type CardEdition, } from "./components/PlayingCard.js";
|
|
19
22
|
export { DeckSprite, DECK_SPRITE_POS, STAKE_SPRITE_POS, type DeckSpriteProps, } from "./components/DeckSprite.js";
|
|
20
23
|
export { MotelyVersionBadge, type MotelyVersionBadgeProps, type MotelyCapabilities, } from "./components/MotelyVersionBadge.js";
|
|
21
24
|
export { extractVisualJamlItems, type JamlPreviewGroups, type JamlPreviewItem, type JamlPreviewSection, type JamlPreviewVisualType, } from "./utils/jamlMapPreview.js";
|
|
22
25
|
export { useMotelyStream, type StreamItem, type StreamState } from "./hooks/useShopStream.js";
|
|
23
26
|
export { useSearch, type SearchResult, type SearchStatus, type UseSearchState, } from "./hooks/useSearch.js";
|
|
24
|
-
export { useAnalyzer, type AnalyzerStatus, } from "./hooks/useAnalyzer.js";
|
|
27
|
+
export { useAnalyzer, type AnalyzerStatus, type AnalyzerLive, type MotelyJsRunState, } from "./hooks/useAnalyzer.js";
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export { Layer } from "./render/Layer.js";
|
|
|
4
4
|
export { JamlCardRenderer } from "./render/CanvasRenderer.js";
|
|
5
5
|
export { JamlGameCard, JamlVoucher, JamlTag, JamlBoss, resolveAnalyzerShopItem, } from "./components/GameCard.js";
|
|
6
6
|
export { AnalyzerExplorer, } from "./components/AnalyzerExplorer.js";
|
|
7
|
+
export { JamlAnalyzerFullscreen, } from "./components/JamlAnalyzerFullscreen.js";
|
|
8
|
+
export { ANALYZER_STREAM_META, DEFAULT_ENABLED_STREAMS, } from "./hooks/analyzerStreamRegistry.js";
|
|
7
9
|
export { JamlMapPreview } from "./components/JamlMapPreview.js";
|
|
8
10
|
export { JamlIde, } from "./components/JamlIde.js";
|
|
9
11
|
export { JamlIdeVisual, } from "./components/JamlIdeVisual.js";
|
|
@@ -17,6 +19,7 @@ export { JimboTooltip, } from "./ui/jimboTooltip.js";
|
|
|
17
19
|
export { JamlIdeToolbar, } from "./components/JamlIdeToolbar.js";
|
|
18
20
|
export { CardList } from "./components/CardList.js";
|
|
19
21
|
export { CardFan } from "./components/CardFan.js";
|
|
22
|
+
export { RealPlayingCard, } from "./components/PlayingCard.js";
|
|
20
23
|
export { DeckSprite, DECK_SPRITE_POS, STAKE_SPRITE_POS, } from "./components/DeckSprite.js";
|
|
21
24
|
export { MotelyVersionBadge, } from "./components/MotelyVersionBadge.js";
|
|
22
25
|
export { extractVisualJamlItems, } from "./utils/jamlMapPreview.js";
|