jaml-ui 0.1.0 → 0.2.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/README.md +27 -0
- package/dist/components/GameCard.d.ts +8 -4
- package/dist/components/GameCard.js +8 -8
- package/dist/data/balatro-jokers.json +1241 -0
- package/dist/decode/motelyItemDecoder.d.ts +49 -5
- package/dist/decode/motelyItemDecoder.js +227 -8
- package/dist/motely.d.ts +1 -1
- package/dist/motely.js +1 -1
- package/dist/r3f/BalatroJokerMesh3D.d.ts +8 -0
- package/dist/r3f/BalatroJokerMesh3D.js +98 -0
- package/dist/r3f/BalatroJokerPreview3D.d.ts +14 -0
- package/dist/r3f/BalatroJokerPreview3D.js +30 -0
- package/dist/r3f/BalatroPlayingCard3D.d.ts +22 -0
- package/dist/r3f/BalatroPlayingCard3D.js +62 -0
- package/dist/r3f/cardConstants.d.ts +16 -0
- package/dist/r3f/cardConstants.js +14 -0
- package/dist/r3f/compositedAtlas.d.ts +5 -0
- package/dist/r3f/compositedAtlas.js +56 -0
- package/dist/r3f/gridUV.d.ts +22 -0
- package/dist/r3f/gridUV.js +30 -0
- package/dist/r3f/index.d.ts +12 -0
- package/dist/r3f/index.js +13 -0
- package/dist/r3f/jokerRegistry.d.ts +28 -0
- package/dist/r3f/jokerRegistry.js +40 -0
- package/dist/r3f/jokerTilt.d.ts +8 -0
- package/dist/r3f/jokerTilt.js +41 -0
- package/dist/r3f/magneticTilt.d.ts +18 -0
- package/dist/r3f/magneticTilt.js +34 -0
- package/dist/r3f/playingCardTypes.d.ts +24 -0
- package/dist/r3f/playingCardTypes.js +32 -0
- package/dist/r3f/playingCardVisuals.d.ts +7 -0
- package/dist/r3f/playingCardVisuals.js +45 -0
- package/dist/r3f/usePlayingCardTexture.d.ts +7 -0
- package/dist/r3f/usePlayingCardTexture.js +92 -0
- package/dist/render/CanvasRenderer.d.ts +2 -1
- package/dist/render/CanvasRenderer.js +31 -3
- package/package.json +30 -10
|
@@ -6,18 +6,62 @@
|
|
|
6
6
|
* 0x1000 = PlayingCard, 0x2000 = Spectral, 0x3000 = Tarot,
|
|
7
7
|
* 0x4000 = Planet, 0x5000 = Joker, 0xF000 = Invalid
|
|
8
8
|
*/
|
|
9
|
+
import { type CardCategory } from "../utils/itemUtils.js";
|
|
10
|
+
export type MotelyItemInput = number | MotelyRuntimeItem | null | undefined;
|
|
11
|
+
export interface MotelyRuntimeItem {
|
|
12
|
+
type?: number;
|
|
13
|
+
value?: number;
|
|
14
|
+
edition?: number;
|
|
15
|
+
seal?: number;
|
|
16
|
+
enhancement?: number;
|
|
17
|
+
suit?: number;
|
|
18
|
+
rank?: number;
|
|
19
|
+
}
|
|
20
|
+
export type MotelyRenderableCategory = CardCategory | "unknown";
|
|
21
|
+
export interface DecodedMotelyItem {
|
|
22
|
+
itemType: number;
|
|
23
|
+
enumKey: string;
|
|
24
|
+
displayName: string;
|
|
25
|
+
category: MotelyRenderableCategory;
|
|
26
|
+
edition: "Foil" | "Holographic" | "Polychrome" | "Negative" | null;
|
|
27
|
+
seal: "Gold" | "Red" | "Blue" | "Purple" | null;
|
|
28
|
+
enhancement: string | null;
|
|
29
|
+
rank: string | null;
|
|
30
|
+
suit: "Clubs" | "Diamonds" | "Hearts" | "Spades" | null;
|
|
31
|
+
}
|
|
32
|
+
export interface MotelyJamlCard {
|
|
33
|
+
type: "joker" | "consumable" | "playing";
|
|
34
|
+
card: {
|
|
35
|
+
name: string;
|
|
36
|
+
edition?: "Foil" | "Holographic" | "Polychrome" | "Negative";
|
|
37
|
+
seal?: string;
|
|
38
|
+
enhancements?: string[];
|
|
39
|
+
rank?: string;
|
|
40
|
+
suit?: string;
|
|
41
|
+
scale?: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export declare function resolveMotelyItemType(input: MotelyItemInput): number | null;
|
|
45
|
+
export declare function motelyItemRenderCategory(input: MotelyItemInput): MotelyRenderableCategory;
|
|
46
|
+
export declare function motelyItemEditionName(input: MotelyItemInput): "Foil" | "Holographic" | "Polychrome" | "Negative" | null;
|
|
47
|
+
export declare function motelyItemSealName(input: MotelyItemInput): "Gold" | "Red" | "Blue" | "Purple" | null;
|
|
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;
|
|
9
51
|
/** Get the enum key name for a MotelyItemType value. */
|
|
10
|
-
export declare function motelyItemTypeName(
|
|
52
|
+
export declare function motelyItemTypeName(input: MotelyItemInput): string;
|
|
11
53
|
/** Get the category string for a MotelyItemType value. */
|
|
12
|
-
export declare function motelyItemCategory(
|
|
54
|
+
export declare function motelyItemCategory(input: MotelyItemInput): string;
|
|
13
55
|
/** Convert PascalCase enum key to display name. */
|
|
14
|
-
export declare function motelyItemDisplayName(
|
|
56
|
+
export declare function motelyItemDisplayName(input: MotelyItemInput): string;
|
|
57
|
+
export declare function decodeMotelyItem(input: MotelyItemInput): DecodedMotelyItem | null;
|
|
58
|
+
export declare function decodeMotelyItemToJamlCard(input: MotelyItemInput, scale?: number): MotelyJamlCard | null;
|
|
15
59
|
/**
|
|
16
60
|
* Decode a MotelyItemType integer to a display name for sprite lookup.
|
|
17
61
|
* Cached per value.
|
|
18
62
|
*/
|
|
19
|
-
export declare function decodeMotelyItemName(
|
|
63
|
+
export declare function decodeMotelyItemName(input: MotelyItemInput): string | null;
|
|
20
64
|
/** Warm the cache for a batch of item type values. */
|
|
21
|
-
export declare function warmMotelyItemCache(itemTypes: readonly
|
|
65
|
+
export declare function warmMotelyItemCache(itemTypes: readonly MotelyItemInput[]): void;
|
|
22
66
|
/** Number of unique items decoded so far. */
|
|
23
67
|
export declare function motelyItemCacheSize(): number;
|
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
* 0x4000 = Planet, 0x5000 = Joker, 0xF000 = Invalid
|
|
8
8
|
*/
|
|
9
9
|
import { Motely } from "motely-wasm";
|
|
10
|
+
import { getItemCategory, getItemDisplayName } from "../utils/itemUtils.js";
|
|
10
11
|
// ─── Category from MotelyItemType integer ────────────────────────────────────
|
|
11
12
|
const CATEGORY_MASK = 0xf000;
|
|
13
|
+
const VALUE_TYPE_MASK = 0xffff;
|
|
14
|
+
const VALUE_SEAL_MASK = 0x70000;
|
|
15
|
+
const VALUE_ENHANCEMENT_MASK = 0x780000;
|
|
16
|
+
const VALUE_EDITION_MASK = 0x3800000;
|
|
12
17
|
const CATEGORY_TO_TYPE = {
|
|
13
18
|
0x1000: "Playing Card",
|
|
14
19
|
0x2000: "Spectral",
|
|
@@ -28,27 +33,238 @@ function ensureItemTypeMap() {
|
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
}
|
|
36
|
+
function asRuntimeItem(input) {
|
|
37
|
+
return input !== null && typeof input === "object" ? input : null;
|
|
38
|
+
}
|
|
39
|
+
function finiteNumber(value) {
|
|
40
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
41
|
+
}
|
|
42
|
+
function runtimeEnumName(enumObject, value) {
|
|
43
|
+
if (value === null)
|
|
44
|
+
return null;
|
|
45
|
+
const enumKey = enumObject[String(value)];
|
|
46
|
+
return typeof enumKey === "string" && enumKey.length > 0 ? enumKey : null;
|
|
47
|
+
}
|
|
48
|
+
function parsePlayingCardEnumKey(enumKey) {
|
|
49
|
+
const match = /^([CDHS])(10|[2-9JQKA])$/.exec(enumKey);
|
|
50
|
+
if (!match)
|
|
51
|
+
return null;
|
|
52
|
+
const suitMap = {
|
|
53
|
+
C: "Clubs",
|
|
54
|
+
D: "Diamonds",
|
|
55
|
+
H: "Hearts",
|
|
56
|
+
S: "Spades",
|
|
57
|
+
};
|
|
58
|
+
const rankMap = {
|
|
59
|
+
J: "Jack",
|
|
60
|
+
Q: "Queen",
|
|
61
|
+
K: "King",
|
|
62
|
+
A: "Ace",
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
rank: rankMap[match[2]] ?? match[2],
|
|
66
|
+
suit: suitMap[match[1]],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function rankNameFromEnum(enumKey) {
|
|
70
|
+
if (!enumKey)
|
|
71
|
+
return null;
|
|
72
|
+
const rankMap = {
|
|
73
|
+
Two: "2",
|
|
74
|
+
Three: "3",
|
|
75
|
+
Four: "4",
|
|
76
|
+
Five: "5",
|
|
77
|
+
Six: "6",
|
|
78
|
+
Seven: "7",
|
|
79
|
+
Eight: "8",
|
|
80
|
+
Nine: "9",
|
|
81
|
+
Ten: "10",
|
|
82
|
+
Jack: "Jack",
|
|
83
|
+
Queen: "Queen",
|
|
84
|
+
King: "King",
|
|
85
|
+
Ace: "Ace",
|
|
86
|
+
};
|
|
87
|
+
return rankMap[enumKey] ?? null;
|
|
88
|
+
}
|
|
89
|
+
function resolvePackedValue(input) {
|
|
90
|
+
if (typeof input === "number")
|
|
91
|
+
return Number.isFinite(input) ? input : null;
|
|
92
|
+
const runtimeItem = asRuntimeItem(input);
|
|
93
|
+
return finiteNumber(runtimeItem?.value);
|
|
94
|
+
}
|
|
95
|
+
function resolveEditionValue(input) {
|
|
96
|
+
const runtimeItem = asRuntimeItem(input);
|
|
97
|
+
const direct = finiteNumber(runtimeItem?.edition);
|
|
98
|
+
if (direct !== null)
|
|
99
|
+
return direct;
|
|
100
|
+
const packedValue = resolvePackedValue(input);
|
|
101
|
+
return packedValue !== null ? packedValue & VALUE_EDITION_MASK : null;
|
|
102
|
+
}
|
|
103
|
+
function resolveSealValue(input) {
|
|
104
|
+
const runtimeItem = asRuntimeItem(input);
|
|
105
|
+
const direct = finiteNumber(runtimeItem?.seal);
|
|
106
|
+
if (direct !== null)
|
|
107
|
+
return direct;
|
|
108
|
+
const packedValue = resolvePackedValue(input);
|
|
109
|
+
return packedValue !== null ? packedValue & VALUE_SEAL_MASK : null;
|
|
110
|
+
}
|
|
111
|
+
function resolveEnhancementValue(input) {
|
|
112
|
+
const runtimeItem = asRuntimeItem(input);
|
|
113
|
+
const direct = finiteNumber(runtimeItem?.enhancement);
|
|
114
|
+
if (direct !== null)
|
|
115
|
+
return direct;
|
|
116
|
+
const packedValue = resolvePackedValue(input);
|
|
117
|
+
return packedValue !== null ? packedValue & VALUE_ENHANCEMENT_MASK : null;
|
|
118
|
+
}
|
|
119
|
+
export function resolveMotelyItemType(input) {
|
|
120
|
+
if (typeof input === "number")
|
|
121
|
+
return Number.isFinite(input) ? (input & VALUE_TYPE_MASK) : null;
|
|
122
|
+
const runtimeItem = asRuntimeItem(input);
|
|
123
|
+
const directType = finiteNumber(runtimeItem?.type);
|
|
124
|
+
if (directType !== null)
|
|
125
|
+
return directType & VALUE_TYPE_MASK;
|
|
126
|
+
const packedValue = finiteNumber(runtimeItem?.value);
|
|
127
|
+
return packedValue !== null ? packedValue & VALUE_TYPE_MASK : null;
|
|
128
|
+
}
|
|
129
|
+
export function motelyItemRenderCategory(input) {
|
|
130
|
+
const enumKey = motelyItemTypeName(input);
|
|
131
|
+
return enumKey === "Unknown" ? "unknown" : getItemCategory(enumKey);
|
|
132
|
+
}
|
|
133
|
+
export function motelyItemEditionName(input) {
|
|
134
|
+
const enumKey = runtimeEnumName(Motely.MotelyItemEdition, resolveEditionValue(input));
|
|
135
|
+
return enumKey === null || enumKey === "None" ? null : enumKey;
|
|
136
|
+
}
|
|
137
|
+
export function motelyItemSealName(input) {
|
|
138
|
+
const enumKey = runtimeEnumName(Motely.MotelyItemSeal, resolveSealValue(input));
|
|
139
|
+
return enumKey === null || enumKey === "None" ? null : enumKey;
|
|
140
|
+
}
|
|
141
|
+
export function motelyItemEnhancementName(input) {
|
|
142
|
+
const enumKey = runtimeEnumName(Motely.MotelyItemEnhancement, resolveEnhancementValue(input));
|
|
143
|
+
return enumKey === null || enumKey === "None" ? null : enumKey;
|
|
144
|
+
}
|
|
145
|
+
export function motelyPlayingCardSuitName(input) {
|
|
146
|
+
const runtimeItem = asRuntimeItem(input);
|
|
147
|
+
const directSuit = runtimeEnumName(Motely.MotelyPlayingCardSuit, finiteNumber(runtimeItem?.suit));
|
|
148
|
+
if (directSuit === "Clubs" || directSuit === "Diamonds" || directSuit === "Hearts" || directSuit === "Spades") {
|
|
149
|
+
return directSuit;
|
|
150
|
+
}
|
|
151
|
+
const parsed = parsePlayingCardEnumKey(motelyItemTypeName(input));
|
|
152
|
+
return parsed?.suit ?? null;
|
|
153
|
+
}
|
|
154
|
+
export function motelyPlayingCardRankName(input) {
|
|
155
|
+
const runtimeItem = asRuntimeItem(input);
|
|
156
|
+
const directRank = runtimeEnumName(Motely.MotelyPlayingCardRank, finiteNumber(runtimeItem?.rank));
|
|
157
|
+
const normalizedDirect = rankNameFromEnum(directRank);
|
|
158
|
+
if (normalizedDirect !== null)
|
|
159
|
+
return normalizedDirect;
|
|
160
|
+
const parsed = parsePlayingCardEnumKey(motelyItemTypeName(input));
|
|
161
|
+
return parsed?.rank ?? null;
|
|
162
|
+
}
|
|
31
163
|
/** Get the enum key name for a MotelyItemType value. */
|
|
32
|
-
export function motelyItemTypeName(
|
|
164
|
+
export function motelyItemTypeName(input) {
|
|
165
|
+
const itemType = resolveMotelyItemType(input);
|
|
166
|
+
if (itemType === null)
|
|
167
|
+
return "Unknown";
|
|
33
168
|
ensureItemTypeMap();
|
|
34
169
|
return _itemTypeToName.get(itemType) ?? "Unknown";
|
|
35
170
|
}
|
|
36
171
|
/** Get the category string for a MotelyItemType value. */
|
|
37
|
-
export function motelyItemCategory(
|
|
172
|
+
export function motelyItemCategory(input) {
|
|
173
|
+
const itemType = resolveMotelyItemType(input);
|
|
174
|
+
if (itemType === null)
|
|
175
|
+
return "Unknown";
|
|
176
|
+
const renderCategory = motelyItemRenderCategory(itemType);
|
|
177
|
+
if (renderCategory === "playing")
|
|
178
|
+
return "Playing Card";
|
|
179
|
+
if (renderCategory === "spectral")
|
|
180
|
+
return "Spectral";
|
|
181
|
+
if (renderCategory === "tarot")
|
|
182
|
+
return "Tarot";
|
|
183
|
+
if (renderCategory === "planet")
|
|
184
|
+
return "Planet";
|
|
185
|
+
if (renderCategory === "joker")
|
|
186
|
+
return "Joker";
|
|
38
187
|
return CATEGORY_TO_TYPE[itemType & CATEGORY_MASK] ?? "Unknown";
|
|
39
188
|
}
|
|
40
189
|
/** Convert PascalCase enum key to display name. */
|
|
41
|
-
export function motelyItemDisplayName(
|
|
42
|
-
const
|
|
43
|
-
return
|
|
190
|
+
export function motelyItemDisplayName(input) {
|
|
191
|
+
const enumKey = motelyItemTypeName(input);
|
|
192
|
+
return enumKey === "Unknown" ? "Unknown" : getItemDisplayName(enumKey);
|
|
44
193
|
}
|
|
45
194
|
// ─── Module-level cache ──────────────────────────────────────────────────────
|
|
46
195
|
const _cache = new Map();
|
|
196
|
+
const _decodedBaseCache = new Map();
|
|
197
|
+
export function decodeMotelyItem(input) {
|
|
198
|
+
const itemType = resolveMotelyItemType(input);
|
|
199
|
+
if (itemType === null)
|
|
200
|
+
return null;
|
|
201
|
+
let base = _decodedBaseCache.get(itemType) ?? null;
|
|
202
|
+
if (!_decodedBaseCache.has(itemType)) {
|
|
203
|
+
const enumKey = motelyItemTypeName(itemType);
|
|
204
|
+
if (enumKey === "Unknown") {
|
|
205
|
+
_decodedBaseCache.set(itemType, null);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const category = motelyItemRenderCategory(itemType);
|
|
209
|
+
const rank = motelyPlayingCardRankName(itemType);
|
|
210
|
+
const suit = motelyPlayingCardSuitName(itemType);
|
|
211
|
+
base = {
|
|
212
|
+
itemType,
|
|
213
|
+
enumKey,
|
|
214
|
+
displayName: category === "playing" && rank && suit ? `${rank} of ${suit}` : getItemDisplayName(enumKey),
|
|
215
|
+
category,
|
|
216
|
+
rank,
|
|
217
|
+
suit,
|
|
218
|
+
};
|
|
219
|
+
_decodedBaseCache.set(itemType, base);
|
|
220
|
+
}
|
|
221
|
+
if (!base)
|
|
222
|
+
return null;
|
|
223
|
+
const decoded = {
|
|
224
|
+
...base,
|
|
225
|
+
edition: motelyItemEditionName(input),
|
|
226
|
+
seal: motelyItemSealName(input),
|
|
227
|
+
enhancement: motelyItemEnhancementName(input),
|
|
228
|
+
};
|
|
229
|
+
return decoded;
|
|
230
|
+
}
|
|
231
|
+
export function decodeMotelyItemToJamlCard(input, scale = 1) {
|
|
232
|
+
const decoded = decodeMotelyItem(input);
|
|
233
|
+
if (!decoded || decoded.category === "unknown")
|
|
234
|
+
return null;
|
|
235
|
+
if (decoded.category === "playing") {
|
|
236
|
+
if (!decoded.rank || !decoded.suit)
|
|
237
|
+
return null;
|
|
238
|
+
return {
|
|
239
|
+
type: "playing",
|
|
240
|
+
card: {
|
|
241
|
+
name: `${decoded.rank} of ${decoded.suit}`,
|
|
242
|
+
edition: decoded.edition ?? undefined,
|
|
243
|
+
seal: decoded.seal ? `${decoded.seal} Seal` : undefined,
|
|
244
|
+
enhancements: decoded.enhancement ? [decoded.enhancement] : undefined,
|
|
245
|
+
rank: decoded.rank,
|
|
246
|
+
suit: decoded.suit,
|
|
247
|
+
scale,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
type: decoded.category === "joker" ? "joker" : "consumable",
|
|
253
|
+
card: {
|
|
254
|
+
name: decoded.displayName,
|
|
255
|
+
edition: decoded.edition ?? undefined,
|
|
256
|
+
scale,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
47
260
|
/**
|
|
48
261
|
* Decode a MotelyItemType integer to a display name for sprite lookup.
|
|
49
262
|
* Cached per value.
|
|
50
263
|
*/
|
|
51
|
-
export function decodeMotelyItemName(
|
|
264
|
+
export function decodeMotelyItemName(input) {
|
|
265
|
+
const itemType = resolveMotelyItemType(input);
|
|
266
|
+
if (itemType === null)
|
|
267
|
+
return null;
|
|
52
268
|
if (_cache.has(itemType))
|
|
53
269
|
return _cache.get(itemType) ?? null;
|
|
54
270
|
const category = motelyItemCategory(itemType);
|
|
@@ -56,14 +272,17 @@ export function decodeMotelyItemName(itemType) {
|
|
|
56
272
|
_cache.set(itemType, null);
|
|
57
273
|
return null;
|
|
58
274
|
}
|
|
59
|
-
const
|
|
275
|
+
const decoded = decodeMotelyItem(input);
|
|
276
|
+
const name = decoded?.displayName ?? motelyItemDisplayName(itemType);
|
|
60
277
|
_cache.set(itemType, name);
|
|
61
278
|
return name;
|
|
62
279
|
}
|
|
63
280
|
/** Warm the cache for a batch of item type values. */
|
|
64
281
|
export function warmMotelyItemCache(itemTypes) {
|
|
65
|
-
for (const t of itemTypes)
|
|
282
|
+
for (const t of itemTypes) {
|
|
66
283
|
decodeMotelyItemName(t);
|
|
284
|
+
decodeMotelyItem(t);
|
|
285
|
+
}
|
|
67
286
|
}
|
|
68
287
|
/** Number of unique items decoded so far. */
|
|
69
288
|
export function motelyItemCacheSize() {
|
package/dist/motely.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { motelyItemTypeName, motelyItemCategory, motelyItemDisplayName, decodeMotelyItemName, warmMotelyItemCache, motelyItemCacheSize, } from "./decode/motelyItemDecoder.js";
|
|
1
|
+
export { decodeMotelyItem, decodeMotelyItemToJamlCard, motelyItemTypeName, motelyItemCategory, motelyItemDisplayName, motelyItemRenderCategory, motelyItemEditionName, motelyItemSealName, motelyItemEnhancementName, motelyPlayingCardRankName, motelyPlayingCardSuitName, decodeMotelyItemName, resolveMotelyItemType, warmMotelyItemCache, motelyItemCacheSize, type DecodedMotelyItem, type MotelyItemInput, type MotelyJamlCard, type MotelyRenderableCategory, type MotelyRuntimeItem, } from "./decode/motelyItemDecoder.js";
|
package/dist/motely.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
export { motelyItemTypeName, motelyItemCategory, motelyItemDisplayName, decodeMotelyItemName, warmMotelyItemCache, motelyItemCacheSize, } from "./decode/motelyItemDecoder.js";
|
|
2
|
+
export { decodeMotelyItem, decodeMotelyItemToJamlCard, motelyItemTypeName, motelyItemCategory, motelyItemDisplayName, motelyItemRenderCategory, motelyItemEditionName, motelyItemSealName, motelyItemEnhancementName, motelyPlayingCardRankName, motelyPlayingCardSuitName, decodeMotelyItemName, resolveMotelyItemType, warmMotelyItemCache, motelyItemCacheSize, } from "./decode/motelyItemDecoder.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface BalatroJokerMesh3DProps {
|
|
2
|
+
displayName: string;
|
|
3
|
+
onAtlasOk?: () => void;
|
|
4
|
+
onAtlasMissing?: () => void;
|
|
5
|
+
/** Override joker sheet URL (default: resolveJamlAssetUrl("jokers")). */
|
|
6
|
+
jokersImageUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function BalatroJokerMesh3D({ displayName, onAtlasOk, onAtlasMissing, jokersImageUrl, }: BalatroJokerMesh3DProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useFrame } from "@react-three/fiber";
|
|
5
|
+
import * as THREE from "three";
|
|
6
|
+
import { applyBalatroGridUV, getLoadedTexturePixelSize } from "./gridUV.js";
|
|
7
|
+
import { findJokerByDisplayName, getJokerAtlasGridSize, getJokerAtlasImageUrl, } from "./jokerRegistry.js";
|
|
8
|
+
import { createZeroMagneticTarget, resetMagneticTarget } from "./magneticTilt.js";
|
|
9
|
+
import { jokerAmbientTiltAtTime, jokerPointerTiltFromUv, JOKER_TILT_LERP_IN, JOKER_TILT_LERP_OUT, stableIdFraction, } from "./jokerTilt.js";
|
|
10
|
+
export function BalatroJokerMesh3D({ displayName, onAtlasOk, onAtlasMissing, jokersImageUrl, }) {
|
|
11
|
+
const center = useMemo(() => findJokerByDisplayName(displayName), [displayName]);
|
|
12
|
+
const grid = useMemo(() => getJokerAtlasGridSize(), []);
|
|
13
|
+
const atlasUrl = jokersImageUrl ?? getJokerAtlasImageUrl();
|
|
14
|
+
const tiltGroupRef = useRef(null);
|
|
15
|
+
const magneticTarget = useRef(createZeroMagneticTarget());
|
|
16
|
+
const [map, setMap] = useState(null);
|
|
17
|
+
const [hovered, setHovered] = useState(false);
|
|
18
|
+
const loadSerial = useRef(0);
|
|
19
|
+
const idFrac = useMemo(() => stableIdFraction(displayName), [displayName]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!center)
|
|
22
|
+
return;
|
|
23
|
+
const serial = ++loadSerial.current;
|
|
24
|
+
let cancelled = false;
|
|
25
|
+
let loadedTex = null;
|
|
26
|
+
const loader = new THREE.TextureLoader();
|
|
27
|
+
loader.load(atlasUrl, (tex) => {
|
|
28
|
+
if (cancelled || serial !== loadSerial.current) {
|
|
29
|
+
tex.dispose();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
loadedTex = tex;
|
|
33
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
34
|
+
tex.magFilter = THREE.NearestFilter;
|
|
35
|
+
tex.minFilter = THREE.NearestFilter;
|
|
36
|
+
const { width: tw, height: th } = getLoadedTexturePixelSize(tex.image);
|
|
37
|
+
const cellW = tw / grid.cols;
|
|
38
|
+
const cellH = th / grid.rows;
|
|
39
|
+
applyBalatroGridUV(tex, center.pos, {
|
|
40
|
+
cellW,
|
|
41
|
+
cellH,
|
|
42
|
+
textureWidth: tw,
|
|
43
|
+
textureHeight: th,
|
|
44
|
+
});
|
|
45
|
+
setMap(tex);
|
|
46
|
+
onAtlasOk?.();
|
|
47
|
+
}, undefined, () => {
|
|
48
|
+
if (!cancelled && serial === loadSerial.current) {
|
|
49
|
+
setMap(null);
|
|
50
|
+
onAtlasMissing?.();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return () => {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
loadedTex?.dispose();
|
|
56
|
+
setMap(null);
|
|
57
|
+
};
|
|
58
|
+
}, [center, displayName, grid.cols, grid.rows, atlasUrl, onAtlasMissing, onAtlasOk]);
|
|
59
|
+
useFrame((state, dt) => {
|
|
60
|
+
const g = tiltGroupRef.current;
|
|
61
|
+
if (!g)
|
|
62
|
+
return;
|
|
63
|
+
if (!hovered) {
|
|
64
|
+
jokerAmbientTiltAtTime(state.clock.elapsedTime, idFrac, magneticTarget.current);
|
|
65
|
+
}
|
|
66
|
+
const tr = magneticTarget.current;
|
|
67
|
+
const a = 1 - Math.exp(-(hovered ? JOKER_TILT_LERP_IN : JOKER_TILT_LERP_OUT) * dt);
|
|
68
|
+
g.rotation.x = THREE.MathUtils.lerp(g.rotation.x, tr.rx, a);
|
|
69
|
+
g.rotation.y = THREE.MathUtils.lerp(g.rotation.y, tr.ry, a);
|
|
70
|
+
g.rotation.z = THREE.MathUtils.lerp(g.rotation.z, tr.rz, a);
|
|
71
|
+
g.position.x = THREE.MathUtils.lerp(g.position.x, tr.ox, a);
|
|
72
|
+
g.position.y = THREE.MathUtils.lerp(g.position.y, tr.oy, a);
|
|
73
|
+
});
|
|
74
|
+
if (!center || !map)
|
|
75
|
+
return null;
|
|
76
|
+
const { width: tw, height: th } = getLoadedTexturePixelSize(map.image);
|
|
77
|
+
const cellW = tw / grid.cols;
|
|
78
|
+
const cellH = th / grid.rows;
|
|
79
|
+
const aspect = cellW / cellH;
|
|
80
|
+
const h = 2.4;
|
|
81
|
+
const w = aspect * h;
|
|
82
|
+
return (_jsx("group", { ref: tiltGroupRef, children: _jsxs("mesh", { onPointerMove: (e) => {
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
const uv = e.uv;
|
|
85
|
+
if (!uv)
|
|
86
|
+
return;
|
|
87
|
+
jokerPointerTiltFromUv(uv, magneticTarget.current);
|
|
88
|
+
}, onPointerEnter: (e) => {
|
|
89
|
+
e.stopPropagation();
|
|
90
|
+
setHovered(true);
|
|
91
|
+
document.body.style.cursor = "pointer";
|
|
92
|
+
}, onPointerLeave: (e) => {
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
setHovered(false);
|
|
95
|
+
resetMagneticTarget(magneticTarget.current);
|
|
96
|
+
document.body.style.cursor = "auto";
|
|
97
|
+
}, children: [_jsx("planeGeometry", { args: [w, h] }), _jsx("meshBasicMaterial", { map: map, transparent: true, toneMapped: false })] }) }));
|
|
98
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
export interface BalatroJokerPreview3DProps {
|
|
3
|
+
displayName: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
style?: CSSProperties;
|
|
6
|
+
canvasHeight?: number;
|
|
7
|
+
jokersImageUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Self-contained Canvas + joker plane with magnetic tilt (for classifier / tool previews).
|
|
11
|
+
*/
|
|
12
|
+
export declare function BalatroJokerPreview3D({ displayName, className, style, canvasHeight, jokersImageUrl, }: BalatroJokerPreview3DProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
/** @deprecated Use {@link BalatroJokerPreview3D}; kept for existing imports. */
|
|
14
|
+
export declare const BalatroJokerThreePreview: typeof BalatroJokerPreview3D;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Canvas } from "@react-three/fiber";
|
|
4
|
+
import { Suspense, useMemo, useState } from "react";
|
|
5
|
+
import { findJokerByDisplayName, getJokerAtlasGridSize } from "./jokerRegistry.js";
|
|
6
|
+
import { BalatroJokerMesh3D } from "./BalatroJokerMesh3D.js";
|
|
7
|
+
/**
|
|
8
|
+
* Self-contained Canvas + joker plane with magnetic tilt (for classifier / tool previews).
|
|
9
|
+
*/
|
|
10
|
+
export function BalatroJokerPreview3D({ displayName, className, style, canvasHeight = 272, jokersImageUrl, }) {
|
|
11
|
+
const center = useMemo(() => findJokerByDisplayName(displayName), [displayName]);
|
|
12
|
+
const grid = useMemo(() => getJokerAtlasGridSize(), []);
|
|
13
|
+
const [atlasMissing, setAtlasMissing] = useState(false);
|
|
14
|
+
const onOk = useMemo(() => () => setAtlasMissing(false), []);
|
|
15
|
+
const onMissing = useMemo(() => () => setAtlasMissing(true), []);
|
|
16
|
+
if (!center) {
|
|
17
|
+
return (_jsx("div", { className: className, style: style, children: _jsxs("div", { style: { color: "#555", fontSize: 11, padding: 8 }, children: ["No P_CENTERS match for \u201C", displayName, "\u201D. Align the row name with the in-game joker name or add an alias in jaml-ui joker registry data."] }) }));
|
|
18
|
+
}
|
|
19
|
+
return (_jsxs("div", { className: className, style: style, children: [_jsx("div", { style: {
|
|
20
|
+
width: "100%",
|
|
21
|
+
height: 300,
|
|
22
|
+
padding: "14px 20px",
|
|
23
|
+
boxSizing: "border-box",
|
|
24
|
+
borderRadius: 6,
|
|
25
|
+
overflow: "visible",
|
|
26
|
+
background: "#0a0a0a",
|
|
27
|
+
}, children: _jsxs(Canvas, { camera: { position: [0, 0, 3.2], fov: 45 }, gl: { alpha: true, antialias: true }, style: { width: "100%", height: canvasHeight, touchAction: "none", display: "block" }, children: [_jsx("ambientLight", { intensity: 0.6 }), _jsx(Suspense, { fallback: null, children: _jsx(BalatroJokerMesh3D, { displayName: displayName, onAtlasOk: onOk, onAtlasMissing: onMissing, jokersImageUrl: jokersImageUrl }) })] }) }), atlasMissing ? (_jsxs("div", { style: { color: "#886644", fontSize: 11, lineHeight: 1.45, marginTop: 8 }, children: ["Add ", _jsx("strong", { children: "Jokers.png" }), " to your public assets or call", " ", _jsx("code", { style: { color: "#aaa" }, children: "setJamlAssetBaseUrl" }), " so", " ", _jsx("code", { style: { color: "#aaa" }, children: "resolveJamlAssetUrl(\"jokers\")" }), " loads. Key", " ", _jsx("code", { style: { color: "#aaa" }, children: center.key }), " \u2192 grid (", center.pos.x, ",", center.pos.y, "); sheet grid ", grid.cols, "\u00D7", grid.rows, "."] })) : null, _jsxs("p", { style: { fontSize: 10, color: "#444", marginTop: 8, marginBottom: 0 }, children: ["UVs from bundled ", _jsx("code", { style: { color: "#555" }, children: "balatro-jokers.json" }), " (grid cells) \u00B7 magnetic tilt approximates Balatro card/sprite hover behavior."] })] }));
|
|
28
|
+
}
|
|
29
|
+
/** @deprecated Use {@link BalatroJokerPreview3D}; kept for existing imports. */
|
|
30
|
+
export const BalatroJokerThreePreview = BalatroJokerPreview3D;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CardBoxOptions, type CardMagnetOptions } from "./cardConstants.js";
|
|
2
|
+
import type { PlayingCard3DModel } from "./playingCardTypes.js";
|
|
3
|
+
export interface BalatroPlayingCard3DProps {
|
|
4
|
+
card: PlayingCard3DModel;
|
|
5
|
+
position?: [number, number, number];
|
|
6
|
+
rotation?: [number, number, number];
|
|
7
|
+
selected?: boolean;
|
|
8
|
+
highlighted?: boolean;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
onPointerEnter?: () => void;
|
|
11
|
+
onPointerLeave?: () => void;
|
|
12
|
+
/** Reserved for future flip animation */
|
|
13
|
+
faceDown?: boolean;
|
|
14
|
+
magnet?: CardMagnetOptions;
|
|
15
|
+
box?: CardBoxOptions;
|
|
16
|
+
/** Extra idle phase for subtle sway (e.g. hand index). */
|
|
17
|
+
idlePhase?: number;
|
|
18
|
+
deckUrl?: string;
|
|
19
|
+
enhancersUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const BalatroPlayingCard3D: import("react").NamedExoticComponent<BalatroPlayingCard3DProps>;
|
|
22
|
+
export default BalatroPlayingCard3D;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useFrame } from "@react-three/fiber";
|
|
5
|
+
import { useSpring, animated } from "@react-spring/three";
|
|
6
|
+
import { DEFAULT_CARD_BOX, DEFAULT_CARD_MAGNET } from "./cardConstants.js";
|
|
7
|
+
import { createZeroMagneticTarget, lerpMagneticGroup, magneticTargetFromUvPlayingCard, resetMagneticTarget, } from "./magneticTilt.js";
|
|
8
|
+
import { editionMaterialProps, enhancementGlowHex, sealColorHex } from "./playingCardVisuals.js";
|
|
9
|
+
import { useCardBackTexture, usePlayingCardFaceTexture } from "./usePlayingCardTexture.js";
|
|
10
|
+
export const BalatroPlayingCard3D = memo(function BalatroPlayingCard3D({ card, position = [0, 0, 0], rotation = [0, 0, 0], selected = false, highlighted = false, onClick, onPointerEnter, onPointerLeave, faceDown: _faceDown = false, magnet: magnetOverrides, box: boxOverrides, idlePhase = 0, deckUrl, enhancersUrl, }) {
|
|
11
|
+
const magnet = { ...DEFAULT_CARD_MAGNET, ...magnetOverrides };
|
|
12
|
+
const box = { ...DEFAULT_CARD_BOX, ...boxOverrides };
|
|
13
|
+
const tiltGroupRef = useRef(null);
|
|
14
|
+
const magneticTarget = useRef(createZeroMagneticTarget());
|
|
15
|
+
const [hovered, setHovered] = useState(false);
|
|
16
|
+
const cardTexture = usePlayingCardFaceTexture(card.suit, card.rank, { deckUrl, enhancersUrl });
|
|
17
|
+
const backTexture = useCardBackTexture();
|
|
18
|
+
const { posY, scale, glowIntensity } = useSpring({
|
|
19
|
+
posY: selected ? 0.3 : hovered ? 0.15 : 0,
|
|
20
|
+
scale: hovered ? 1.08 : selected ? 1.05 : 1,
|
|
21
|
+
glowIntensity: highlighted ? 1 : hovered ? 0.5 : 0,
|
|
22
|
+
config: { tension: 300, friction: 20 },
|
|
23
|
+
});
|
|
24
|
+
useFrame((state, dt) => {
|
|
25
|
+
const g = tiltGroupRef.current;
|
|
26
|
+
if (!g)
|
|
27
|
+
return;
|
|
28
|
+
const idleZ = Math.sin(state.clock.elapsedTime * 0.55 + position[0] * 2.1 + idlePhase) * 0.018;
|
|
29
|
+
lerpMagneticGroup(g, magneticTarget.current, dt, magnet.LERP_IN, magnet.LERP_OUT, hovered, idleZ);
|
|
30
|
+
});
|
|
31
|
+
const editionProps = useMemo(() => editionMaterialProps(card.edition), [card.edition]);
|
|
32
|
+
const enhancementColor = useMemo(() => enhancementGlowHex(card, highlighted), [card, highlighted]);
|
|
33
|
+
const onCardPointerMove = (e) => {
|
|
34
|
+
e.stopPropagation();
|
|
35
|
+
const uv = e.uv;
|
|
36
|
+
if (!uv)
|
|
37
|
+
return;
|
|
38
|
+
Object.assign(magneticTarget.current, magneticTargetFromUvPlayingCard(uv, magnet));
|
|
39
|
+
};
|
|
40
|
+
const onLeave = (e) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
setHovered(false);
|
|
43
|
+
resetMagneticTarget(magneticTarget.current);
|
|
44
|
+
onPointerLeave?.();
|
|
45
|
+
document.body.style.cursor = "auto";
|
|
46
|
+
};
|
|
47
|
+
if (!cardTexture)
|
|
48
|
+
return null;
|
|
49
|
+
return (_jsx(animated.group, { "position-x": position[0], "position-y": posY.to((y) => position[1] + y), "position-z": position[2], "rotation-x": rotation[0], "rotation-y": rotation[1], "rotation-z": rotation[2], scale: scale, children: _jsxs("group", { ref: tiltGroupRef, children: [_jsx(animated.pointLight, { color: enhancementColor, intensity: glowIntensity.to((i) => i * 2), distance: 1, position: [0, 0, 0.1] }), _jsxs("mesh", { onClick: (e) => {
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
const hit = e.intersections[0];
|
|
52
|
+
if (!hit || hit.object !== e.object)
|
|
53
|
+
return;
|
|
54
|
+
onClick?.();
|
|
55
|
+
}, onPointerMove: onCardPointerMove, onPointerEnter: (e) => {
|
|
56
|
+
e.stopPropagation();
|
|
57
|
+
setHovered(true);
|
|
58
|
+
onPointerEnter?.();
|
|
59
|
+
document.body.style.cursor = "pointer";
|
|
60
|
+
}, onPointerLeave: onLeave, castShadow: true, receiveShadow: true, children: [_jsx("boxGeometry", { args: [box.WIDTH, box.HEIGHT, box.DEPTH] }), _jsx("meshBasicMaterial", { attach: "material-4", map: cardTexture, toneMapped: false }), _jsx("meshStandardMaterial", { attach: "material-5", map: backTexture, ...editionProps }), _jsx("meshStandardMaterial", { attach: "material-0", color: "#f5f5dc" }), _jsx("meshStandardMaterial", { attach: "material-1", color: "#f5f5dc" }), _jsx("meshStandardMaterial", { attach: "material-2", color: "#f5f5dc" }), _jsx("meshStandardMaterial", { attach: "material-3", color: "#f5f5dc" })] }), selected ? (_jsxs("mesh", { position: [0, 0, -box.DEPTH], children: [_jsx("ringGeometry", { args: [0.45, 0.5, 32] }), _jsx("meshBasicMaterial", { color: "#f1c40f", transparent: true, opacity: 0.8 })] })) : null, card.enhancement ? (_jsxs("mesh", { position: [box.WIDTH / 2 - 0.1, box.HEIGHT / 2 - 0.1, box.DEPTH + 0.01], children: [_jsx("circleGeometry", { args: [0.08, 16] }), _jsx("meshBasicMaterial", { color: enhancementColor })] })) : null, card.seal ? (_jsxs("mesh", { position: [-box.WIDTH / 2 + 0.1, -box.HEIGHT / 2 + 0.1, box.DEPTH + 0.01], children: [_jsx("circleGeometry", { args: [0.06, 6] }), _jsx("meshBasicMaterial", { color: sealColorHex(card.seal) })] })) : null] }) }));
|
|
61
|
+
});
|
|
62
|
+
export default BalatroPlayingCard3D;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Default magnetic tilt / parallax matching Balatro-style card hover. */
|
|
2
|
+
export declare const DEFAULT_CARD_MAGNET: {
|
|
3
|
+
readonly MAX_TILT_X: 0.36;
|
|
4
|
+
readonly MAX_TILT_Y: 0.42;
|
|
5
|
+
readonly MAX_SHIFT: 0.038;
|
|
6
|
+
readonly TWIST_Z: 0.11;
|
|
7
|
+
readonly LERP_IN: 18;
|
|
8
|
+
readonly LERP_OUT: 10;
|
|
9
|
+
};
|
|
10
|
+
export type CardMagnetOptions = Partial<typeof DEFAULT_CARD_MAGNET>;
|
|
11
|
+
export declare const DEFAULT_CARD_BOX: {
|
|
12
|
+
readonly WIDTH: 0.7;
|
|
13
|
+
readonly HEIGHT: 0.95;
|
|
14
|
+
readonly DEPTH: 0.02;
|
|
15
|
+
};
|
|
16
|
+
export type CardBoxOptions = Partial<typeof DEFAULT_CARD_BOX>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Default magnetic tilt / parallax matching Balatro-style card hover. */
|
|
2
|
+
export const DEFAULT_CARD_MAGNET = {
|
|
3
|
+
MAX_TILT_X: 0.36,
|
|
4
|
+
MAX_TILT_Y: 0.42,
|
|
5
|
+
MAX_SHIFT: 0.038,
|
|
6
|
+
TWIST_Z: 0.11,
|
|
7
|
+
LERP_IN: 18,
|
|
8
|
+
LERP_OUT: 10,
|
|
9
|
+
};
|
|
10
|
+
export const DEFAULT_CARD_BOX = {
|
|
11
|
+
WIDTH: 0.7,
|
|
12
|
+
HEIGHT: 0.95,
|
|
13
|
+
DEPTH: 0.02,
|
|
14
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composite transparent 8Bit deck pips onto Enhancers card fronts (per-cell).
|
|
3
|
+
* Output canvas matches atlas pixel size so UV math is unchanged.
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadCompositedPlayingAtlas(atlasUrl: string, cols: number, rows: number, enhancersUrl?: string): Promise<HTMLCanvasElement>;
|