jaml-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/assets/8BitDeck.png +0 -0
- package/assets/BlindChips.png +0 -0
- package/assets/Boosters.png +0 -0
- package/assets/Editions.png +0 -0
- package/assets/Enhancers.png +0 -0
- package/assets/Jokers.png +0 -0
- package/assets/Tarots.png +0 -0
- package/assets/Vouchers.png +0 -0
- package/assets/stickers.png +0 -0
- package/assets/tags.png +0 -0
- package/dist/assets.d.ts +18 -0
- package/dist/assets.js +66 -0
- package/dist/components/CardList.d.ts +8 -0
- package/dist/components/CardList.js +5 -0
- package/dist/components/GameCard.d.ts +52 -0
- package/dist/components/GameCard.js +351 -0
- package/dist/components/PlayingCard.d.ts +18 -0
- package/dist/components/PlayingCard.js +115 -0
- package/dist/core.d.ts +7 -0
- package/dist/core.js +7 -0
- package/dist/decode/motelyItemDecoder.d.ts +23 -0
- package/dist/decode/motelyItemDecoder.js +71 -0
- package/dist/decode/packedBalatroItem.d.ts +13 -0
- package/dist/decode/packedBalatroItem.js +26 -0
- package/dist/hooks/useShopStream.d.ts +22 -0
- package/dist/hooks/useShopStream.js +82 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/motely.d.ts +1 -0
- package/dist/motely.js +2 -0
- package/dist/render/CanvasRenderer.d.ts +7 -0
- package/dist/render/CanvasRenderer.js +147 -0
- package/dist/render/Layer.d.ts +29 -0
- package/dist/render/Layer.js +18 -0
- package/dist/sprites/spriteData.d.ts +57 -0
- package/dist/sprites/spriteData.js +99 -0
- package/dist/sprites/spriteMapper.d.ts +11 -0
- package/dist/sprites/spriteMapper.js +42 -0
- package/dist/utils/gameCardUtils.d.ts +12 -0
- package/dist/utils/gameCardUtils.js +49 -0
- package/dist/utils/itemUtils.d.ts +11 -0
- package/dist/utils/itemUtils.js +71 -0
- package/package.json +72 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { resolveJamlAssetUrl } from '../assets.js';
|
|
4
|
+
function cn(...classes) { return classes.filter(Boolean).join(" "); }
|
|
5
|
+
const CARD_WIDTH = 71;
|
|
6
|
+
const CARD_HEIGHT = 95;
|
|
7
|
+
// 8BitDeck.png is 13 columns (A,2,3,4,5,6,7,8,9,10,J,Q,K) x 4 rows (Spades, Hearts, Clubs, Diamonds)
|
|
8
|
+
const RANK_TO_COL = {
|
|
9
|
+
'Ace': 0, 'A': 0,
|
|
10
|
+
'2': 1, '3': 2, '4': 3, '5': 4, '6': 5, '7': 6, '8': 7, '9': 8,
|
|
11
|
+
'10': 9,
|
|
12
|
+
'Jack': 10, 'J': 10,
|
|
13
|
+
'Queen': 11, 'Q': 11,
|
|
14
|
+
'King': 12, 'K': 12,
|
|
15
|
+
};
|
|
16
|
+
const SUIT_TO_ROW = {
|
|
17
|
+
'Spades': 0, 'spades': 0,
|
|
18
|
+
'Hearts': 1, 'hearts': 1,
|
|
19
|
+
'Clubs': 2, 'clubs': 2,
|
|
20
|
+
'Diamonds': 3, 'diamonds': 3,
|
|
21
|
+
};
|
|
22
|
+
// Enhancers.png is 7 columns x 5 rows
|
|
23
|
+
// Row 0: Base, Bonus, Mult, Wild, Glass, Steel, Stone
|
|
24
|
+
// Row 1: Gold, Lucky, (seals start)
|
|
25
|
+
const ENHANCEMENT_TO_POS = {
|
|
26
|
+
'bonus': { x: 1, y: 0 },
|
|
27
|
+
'mult': { x: 2, y: 0 },
|
|
28
|
+
'wild': { x: 3, y: 0 },
|
|
29
|
+
'glass': { x: 4, y: 0 },
|
|
30
|
+
'steel': { x: 5, y: 0 },
|
|
31
|
+
'stone': { x: 6, y: 0 },
|
|
32
|
+
'gold': { x: 0, y: 1 },
|
|
33
|
+
'lucky': { x: 1, y: 1 },
|
|
34
|
+
};
|
|
35
|
+
const SEAL_TO_POS = {
|
|
36
|
+
'gold': { x: 2, y: 1 },
|
|
37
|
+
'red': { x: 3, y: 1 },
|
|
38
|
+
'blue': { x: 4, y: 1 },
|
|
39
|
+
'purple': { x: 5, y: 1 },
|
|
40
|
+
};
|
|
41
|
+
const EDITION_TO_POS = {
|
|
42
|
+
'Foil': { x: 0, y: 0 },
|
|
43
|
+
'Holographic': { x: 1, y: 0 },
|
|
44
|
+
'Polychrome': { x: 2, y: 0 },
|
|
45
|
+
'Negative': { x: 3, y: 0 },
|
|
46
|
+
};
|
|
47
|
+
export function RealPlayingCard({ suit, rank, enhancement, seal, edition, className, size = 71, style }) {
|
|
48
|
+
const col = RANK_TO_COL[rank];
|
|
49
|
+
const row = SUIT_TO_ROW[suit];
|
|
50
|
+
if (col === undefined || row === undefined) {
|
|
51
|
+
console.warn(`Invalid card: ${rank} of ${suit}`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const scale = size / CARD_WIDTH;
|
|
55
|
+
const finalH = size * (CARD_HEIGHT / CARD_WIDTH);
|
|
56
|
+
// Base card position
|
|
57
|
+
const bgX = -col * CARD_WIDTH;
|
|
58
|
+
const bgY = -row * CARD_HEIGHT;
|
|
59
|
+
// Enhancement background (if any)
|
|
60
|
+
const enhPos = enhancement ? ENHANCEMENT_TO_POS[enhancement] : { x: 0, y: 0 };
|
|
61
|
+
const enhBgX = -enhPos.x * CARD_WIDTH;
|
|
62
|
+
const enhBgY = -enhPos.y * CARD_HEIGHT;
|
|
63
|
+
// Seal overlay
|
|
64
|
+
const sealPos = seal ? SEAL_TO_POS[seal] : null;
|
|
65
|
+
const sealBgX = sealPos ? -sealPos.x * CARD_WIDTH : 0;
|
|
66
|
+
const sealBgY = sealPos ? -sealPos.y * CARD_HEIGHT : 0;
|
|
67
|
+
// Edition overlay
|
|
68
|
+
const editionPos = edition ? EDITION_TO_POS[edition] : null;
|
|
69
|
+
const editionBgX = editionPos ? -editionPos.x * CARD_WIDTH : 0;
|
|
70
|
+
const editionBgY = editionPos ? -editionPos.y * CARD_HEIGHT : 0;
|
|
71
|
+
const isNegative = edition === 'Negative';
|
|
72
|
+
const baseFilter = isNegative ? 'invert(0.94)' : 'none';
|
|
73
|
+
const enhancersUrl = resolveJamlAssetUrl('enhancers');
|
|
74
|
+
const deckUrl = resolveJamlAssetUrl('deck');
|
|
75
|
+
const editionsUrl = resolveJamlAssetUrl('editions');
|
|
76
|
+
return (_jsxs("div", { className: cn('relative overflow-hidden inline-block select-none', className), style: {
|
|
77
|
+
width: size,
|
|
78
|
+
height: finalH,
|
|
79
|
+
imageRendering: 'pixelated',
|
|
80
|
+
...style
|
|
81
|
+
}, title: `${rank} of ${suit}${enhancement ? ` (${enhancement})` : ''}${seal ? ` [${seal} seal]` : ''}${edition ? ` {${edition}}` : ''}`, children: [_jsx("div", { className: "absolute inset-0", style: {
|
|
82
|
+
backgroundImage: `url(${enhancersUrl})`,
|
|
83
|
+
backgroundPosition: `${enhBgX}px ${enhBgY}px`,
|
|
84
|
+
width: CARD_WIDTH,
|
|
85
|
+
height: CARD_HEIGHT,
|
|
86
|
+
transform: `scale(${scale})`,
|
|
87
|
+
transformOrigin: 'top left',
|
|
88
|
+
backgroundRepeat: 'no-repeat',
|
|
89
|
+
} }), _jsx("div", { className: "absolute inset-0 z-[1]", style: {
|
|
90
|
+
backgroundImage: `url(${deckUrl})`,
|
|
91
|
+
backgroundPosition: `${bgX}px ${bgY}px`,
|
|
92
|
+
width: CARD_WIDTH,
|
|
93
|
+
height: CARD_HEIGHT,
|
|
94
|
+
transform: `scale(${scale})`,
|
|
95
|
+
transformOrigin: 'top left',
|
|
96
|
+
backgroundRepeat: 'no-repeat',
|
|
97
|
+
filter: baseFilter
|
|
98
|
+
} }), edition && edition !== 'Negative' && (_jsx("div", { className: "absolute inset-0 z-[2] mix-blend-screen opacity-60", style: {
|
|
99
|
+
backgroundImage: `url(${editionsUrl})`,
|
|
100
|
+
backgroundPosition: `${editionBgX}px ${editionBgY}px`,
|
|
101
|
+
width: CARD_WIDTH,
|
|
102
|
+
height: CARD_HEIGHT,
|
|
103
|
+
transform: `scale(${scale})`,
|
|
104
|
+
transformOrigin: 'top left',
|
|
105
|
+
backgroundRepeat: 'no-repeat',
|
|
106
|
+
} })), seal && (_jsx("div", { className: "absolute inset-0 z-[3]", style: {
|
|
107
|
+
backgroundImage: `url(${enhancersUrl})`,
|
|
108
|
+
backgroundPosition: `${sealBgX}px ${sealBgY}px`,
|
|
109
|
+
width: CARD_WIDTH,
|
|
110
|
+
height: CARD_HEIGHT,
|
|
111
|
+
transform: `scale(${scale})`,
|
|
112
|
+
transformOrigin: 'top left',
|
|
113
|
+
backgroundRepeat: 'no-repeat',
|
|
114
|
+
} })), isNegative && (_jsx("div", { className: "absolute inset-0 z-[4] bg-red-500/10 pointer-events-none mix-blend-overlay" }))] }));
|
|
115
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { JAML_ASSET_FILES, clearJamlAssetBaseUrl, getDefaultJamlAssetUrlMap, resolveJamlAssetUrl, setJamlAssetBaseUrl, type JamlAssetFile, type JamlAssetKey, } from "./assets.js";
|
|
2
|
+
export { Layer, type LayerOptions } from "./render/Layer.js";
|
|
3
|
+
export { getSpriteData, type SpriteData, type SpriteSheetType } from "./sprites/spriteMapper.js";
|
|
4
|
+
export { SPRITE_SHEETS, JOKERS, JOKER_FACES, TAROTS_AND_PLANETS, CONSUMABLE_FACES, VOUCHERS, BOSSES, TAGS, BOOSTER_PACKS, EDITION_MAP, STICKER_MAP, RANK_MAP, SUIT_MAP, ENHANCER_MAP, SEAL_MAP, type SpritePos, type SpriteEntry, type SpriteSheetInfo, } from "./sprites/spriteData.js";
|
|
5
|
+
export { BalatroItemCategory, packedItemCategory, packedJokerRarity, packedItemIndex, isPackedItemValid, } from "./decode/packedBalatroItem.js";
|
|
6
|
+
export { getItemDisplayName, getItemCategory, getSuitColor } from "./utils/itemUtils.js";
|
|
7
|
+
export { getStandardCardPosition, getSealPosition, getEnhancerPosition } from "./utils/gameCardUtils.js";
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { JAML_ASSET_FILES, clearJamlAssetBaseUrl, getDefaultJamlAssetUrlMap, resolveJamlAssetUrl, setJamlAssetBaseUrl, } from "./assets.js";
|
|
2
|
+
export { Layer } from "./render/Layer.js";
|
|
3
|
+
export { getSpriteData } from "./sprites/spriteMapper.js";
|
|
4
|
+
export { SPRITE_SHEETS, JOKERS, JOKER_FACES, TAROTS_AND_PLANETS, CONSUMABLE_FACES, VOUCHERS, BOSSES, TAGS, BOOSTER_PACKS, EDITION_MAP, STICKER_MAP, RANK_MAP, SUIT_MAP, ENHANCER_MAP, SEAL_MAP, } from "./sprites/spriteData.js";
|
|
5
|
+
export { BalatroItemCategory, packedItemCategory, packedJokerRarity, packedItemIndex, isPackedItemValid, } from "./decode/packedBalatroItem.js";
|
|
6
|
+
export { getItemDisplayName, getItemCategory, getSuitColor } from "./utils/itemUtils.js";
|
|
7
|
+
export { getStandardCardPosition, getSealPosition, getEnhancerPosition } from "./utils/gameCardUtils.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motely item decoder.
|
|
3
|
+
*
|
|
4
|
+
* MotelyItem.Value is a packed integer. The MotelyItemType enum
|
|
5
|
+
* uses packed integers where the top nibble encodes category:
|
|
6
|
+
* 0x1000 = PlayingCard, 0x2000 = Spectral, 0x3000 = Tarot,
|
|
7
|
+
* 0x4000 = Planet, 0x5000 = Joker, 0xF000 = Invalid
|
|
8
|
+
*/
|
|
9
|
+
/** Get the enum key name for a MotelyItemType value. */
|
|
10
|
+
export declare function motelyItemTypeName(itemType: number): string;
|
|
11
|
+
/** Get the category string for a MotelyItemType value. */
|
|
12
|
+
export declare function motelyItemCategory(itemType: number): string;
|
|
13
|
+
/** Convert PascalCase enum key to display name. */
|
|
14
|
+
export declare function motelyItemDisplayName(itemType: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Decode a MotelyItemType integer to a display name for sprite lookup.
|
|
17
|
+
* Cached per value.
|
|
18
|
+
*/
|
|
19
|
+
export declare function decodeMotelyItemName(itemType: number): string | null;
|
|
20
|
+
/** Warm the cache for a batch of item type values. */
|
|
21
|
+
export declare function warmMotelyItemCache(itemTypes: readonly number[]): void;
|
|
22
|
+
/** Number of unique items decoded so far. */
|
|
23
|
+
export declare function motelyItemCacheSize(): number;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motely item decoder.
|
|
3
|
+
*
|
|
4
|
+
* MotelyItem.Value is a packed integer. The MotelyItemType enum
|
|
5
|
+
* uses packed integers where the top nibble encodes category:
|
|
6
|
+
* 0x1000 = PlayingCard, 0x2000 = Spectral, 0x3000 = Tarot,
|
|
7
|
+
* 0x4000 = Planet, 0x5000 = Joker, 0xF000 = Invalid
|
|
8
|
+
*/
|
|
9
|
+
import { Motely } from "motely-wasm";
|
|
10
|
+
// ─── Category from MotelyItemType integer ────────────────────────────────────
|
|
11
|
+
const CATEGORY_MASK = 0xf000;
|
|
12
|
+
const CATEGORY_TO_TYPE = {
|
|
13
|
+
0x1000: "Playing Card",
|
|
14
|
+
0x2000: "Spectral",
|
|
15
|
+
0x3000: "Tarot",
|
|
16
|
+
0x4000: "Planet",
|
|
17
|
+
0x5000: "Joker",
|
|
18
|
+
};
|
|
19
|
+
// ─── Reverse lookup: MotelyItemType integer → string name ────────────────────
|
|
20
|
+
const _itemTypeToName = new Map();
|
|
21
|
+
function ensureItemTypeMap() {
|
|
22
|
+
if (_itemTypeToName.size > 0)
|
|
23
|
+
return;
|
|
24
|
+
const e = Motely.MotelyItemType;
|
|
25
|
+
for (const [key, val] of Object.entries(e)) {
|
|
26
|
+
if (typeof val === "number" && typeof key === "string" && !/^\d+$/.test(key)) {
|
|
27
|
+
_itemTypeToName.set(val, key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Get the enum key name for a MotelyItemType value. */
|
|
32
|
+
export function motelyItemTypeName(itemType) {
|
|
33
|
+
ensureItemTypeMap();
|
|
34
|
+
return _itemTypeToName.get(itemType) ?? "Unknown";
|
|
35
|
+
}
|
|
36
|
+
/** Get the category string for a MotelyItemType value. */
|
|
37
|
+
export function motelyItemCategory(itemType) {
|
|
38
|
+
return CATEGORY_TO_TYPE[itemType & CATEGORY_MASK] ?? "Unknown";
|
|
39
|
+
}
|
|
40
|
+
/** Convert PascalCase enum key to display name. */
|
|
41
|
+
export function motelyItemDisplayName(itemType) {
|
|
42
|
+
const name = motelyItemTypeName(itemType);
|
|
43
|
+
return name.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
44
|
+
}
|
|
45
|
+
// ─── Module-level cache ──────────────────────────────────────────────────────
|
|
46
|
+
const _cache = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Decode a MotelyItemType integer to a display name for sprite lookup.
|
|
49
|
+
* Cached per value.
|
|
50
|
+
*/
|
|
51
|
+
export function decodeMotelyItemName(itemType) {
|
|
52
|
+
if (_cache.has(itemType))
|
|
53
|
+
return _cache.get(itemType) ?? null;
|
|
54
|
+
const category = motelyItemCategory(itemType);
|
|
55
|
+
if (category === "Unknown") {
|
|
56
|
+
_cache.set(itemType, null);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const name = motelyItemDisplayName(itemType);
|
|
60
|
+
_cache.set(itemType, name);
|
|
61
|
+
return name;
|
|
62
|
+
}
|
|
63
|
+
/** Warm the cache for a batch of item type values. */
|
|
64
|
+
export function warmMotelyItemCache(itemTypes) {
|
|
65
|
+
for (const t of itemTypes)
|
|
66
|
+
decodeMotelyItemName(t);
|
|
67
|
+
}
|
|
68
|
+
/** Number of unique items decoded so far. */
|
|
69
|
+
export function motelyItemCacheSize() {
|
|
70
|
+
return _cache.size;
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Bit-packed shop/card ids (Balatro item encoding). */
|
|
2
|
+
export declare const BalatroItemCategory: {
|
|
3
|
+
readonly PlayingCard: 1;
|
|
4
|
+
readonly Spectral: 2;
|
|
5
|
+
readonly Tarot: 3;
|
|
6
|
+
readonly Planet: 4;
|
|
7
|
+
readonly Joker: 5;
|
|
8
|
+
readonly Invalid: 15;
|
|
9
|
+
};
|
|
10
|
+
export declare function packedItemCategory(packed: number): number;
|
|
11
|
+
export declare function packedJokerRarity(packed: number): number;
|
|
12
|
+
export declare function packedItemIndex(packed: number): number;
|
|
13
|
+
export declare function isPackedItemValid(packed: number): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Bit-packed shop/card ids (Balatro item encoding). */
|
|
2
|
+
export const BalatroItemCategory = {
|
|
3
|
+
PlayingCard: 1,
|
|
4
|
+
Spectral: 2,
|
|
5
|
+
Tarot: 3,
|
|
6
|
+
Planet: 4,
|
|
7
|
+
Joker: 5,
|
|
8
|
+
Invalid: 0xf,
|
|
9
|
+
};
|
|
10
|
+
const CATEGORY_OFFSET = 12;
|
|
11
|
+
const CATEGORY_MASK = 0xf000;
|
|
12
|
+
const RARITY_OFFSET = 10;
|
|
13
|
+
const RARITY_MASK = 0x0c00;
|
|
14
|
+
export function packedItemCategory(packed) {
|
|
15
|
+
return (packed & CATEGORY_MASK) >> CATEGORY_OFFSET;
|
|
16
|
+
}
|
|
17
|
+
export function packedJokerRarity(packed) {
|
|
18
|
+
return (packed & RARITY_MASK) >> RARITY_OFFSET;
|
|
19
|
+
}
|
|
20
|
+
export function packedItemIndex(packed) {
|
|
21
|
+
return packed & ~(CATEGORY_MASK | RARITY_MASK);
|
|
22
|
+
}
|
|
23
|
+
export function isPackedItemValid(packed) {
|
|
24
|
+
const category = packedItemCategory(packed);
|
|
25
|
+
return category >= BalatroItemCategory.PlayingCard && category <= BalatroItemCategory.Joker;
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface StreamItem {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
value?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface StreamState {
|
|
7
|
+
items: StreamItem[];
|
|
8
|
+
ready: boolean;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
loadingMore: boolean;
|
|
11
|
+
error: string | null;
|
|
12
|
+
pullMore: (count?: number) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Generic stream hook for iterating any motely-wasm analyzer stream.
|
|
16
|
+
*
|
|
17
|
+
* @param initStream - called once to initialize the stream (e.g. analyzer.initShop(ante))
|
|
18
|
+
* @param nextItem - called to get the next item from the stream (e.g. analyzer.nextShopItem())
|
|
19
|
+
* @param deps - dependency array that triggers stream re-initialization
|
|
20
|
+
* @param initialItems - items already visible (cursor advances past them)
|
|
21
|
+
*/
|
|
22
|
+
export declare function useMotelyStream(initStream: (() => void) | null, nextItem: (() => StreamItem) | null, deps: unknown[], initialItems?: StreamItem[]): StreamState;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
const DEFAULT_PULL = 12;
|
|
4
|
+
/**
|
|
5
|
+
* Generic stream hook for iterating any motely-wasm analyzer stream.
|
|
6
|
+
*
|
|
7
|
+
* @param initStream - called once to initialize the stream (e.g. analyzer.initShop(ante))
|
|
8
|
+
* @param nextItem - called to get the next item from the stream (e.g. analyzer.nextShopItem())
|
|
9
|
+
* @param deps - dependency array that triggers stream re-initialization
|
|
10
|
+
* @param initialItems - items already visible (cursor advances past them)
|
|
11
|
+
*/
|
|
12
|
+
export function useMotelyStream(initStream, nextItem, deps, initialItems = []) {
|
|
13
|
+
const [items, setItems] = useState(() => initialItems.map(i => ({ ...i })));
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
const [ready, setReady] = useState(false);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
18
|
+
const nextRef = useRef(nextItem);
|
|
19
|
+
nextRef.current = nextItem;
|
|
20
|
+
const busyRef = useRef(false);
|
|
21
|
+
const genRef = useRef(0);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const gen = ++genRef.current;
|
|
24
|
+
const base = initialItems.map(i => ({ ...i }));
|
|
25
|
+
setItems(base);
|
|
26
|
+
setError(null);
|
|
27
|
+
setReady(false);
|
|
28
|
+
setLoading(true);
|
|
29
|
+
setLoadingMore(false);
|
|
30
|
+
if (!initStream || !nextItem) {
|
|
31
|
+
setLoading(false);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
initStream();
|
|
36
|
+
// Advance past already-visible items
|
|
37
|
+
for (let i = 0; i < base.length; i++)
|
|
38
|
+
nextItem();
|
|
39
|
+
if (base.length === 0) {
|
|
40
|
+
const prefetch = [];
|
|
41
|
+
for (let i = 0; i < DEFAULT_PULL; i++)
|
|
42
|
+
prefetch.push(nextItem());
|
|
43
|
+
if (gen !== genRef.current)
|
|
44
|
+
return;
|
|
45
|
+
setItems(prefetch);
|
|
46
|
+
}
|
|
47
|
+
setReady(true);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (gen !== genRef.current)
|
|
51
|
+
return;
|
|
52
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (gen === genRef.current)
|
|
56
|
+
setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
|
+
}, deps);
|
|
60
|
+
const pullMore = useCallback((count = DEFAULT_PULL) => {
|
|
61
|
+
const next = nextRef.current;
|
|
62
|
+
if (!next || count <= 0 || busyRef.current)
|
|
63
|
+
return;
|
|
64
|
+
busyRef.current = true;
|
|
65
|
+
setLoadingMore(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
try {
|
|
68
|
+
const batch = [];
|
|
69
|
+
for (let i = 0; i < count; i++)
|
|
70
|
+
batch.push(next());
|
|
71
|
+
setItems(prev => [...prev, ...batch]);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
busyRef.current = false;
|
|
78
|
+
setLoadingMore(false);
|
|
79
|
+
}
|
|
80
|
+
}, []);
|
|
81
|
+
return { items, ready, loading, loadingMore, error, pullMore };
|
|
82
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { JAML_ASSET_FILES, clearJamlAssetBaseUrl, getDefaultJamlAssetUrlMap, resolveJamlAssetUrl, setJamlAssetBaseUrl, type JamlAssetFile, type JamlAssetKey, } from "./assets.js";
|
|
2
|
+
export { Layer, type LayerOptions } from "./render/Layer.js";
|
|
3
|
+
export { JamlCardRenderer, type JamlCardRendererProps } from "./render/CanvasRenderer.js";
|
|
4
|
+
export { JamlGameCard, JamlVoucher, JamlTag, JamlBoss, resolveAnalyzerShopItem, type JamlGameCardProps, type AnalyzerShopItem, type AnalyzerResolvedItem, } from "./components/GameCard.js";
|
|
5
|
+
export { CardList, type CardListProps } from "./components/CardList.js";
|
|
6
|
+
export { useMotelyStream, type StreamItem, type StreamState } from "./hooks/useShopStream.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
export { JAML_ASSET_FILES, clearJamlAssetBaseUrl, getDefaultJamlAssetUrlMap, resolveJamlAssetUrl, setJamlAssetBaseUrl, } from "./assets.js";
|
|
3
|
+
export { Layer } from "./render/Layer.js";
|
|
4
|
+
export { JamlCardRenderer } from "./render/CanvasRenderer.js";
|
|
5
|
+
export { JamlGameCard, JamlVoucher, JamlTag, JamlBoss, resolveAnalyzerShopItem, } from "./components/GameCard.js";
|
|
6
|
+
export { CardList } from "./components/CardList.js";
|
|
7
|
+
export { useMotelyStream } from "./hooks/useShopStream.js";
|
package/dist/motely.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { motelyItemTypeName, motelyItemCategory, motelyItemDisplayName, decodeMotelyItemName, warmMotelyItemCache, motelyItemCacheSize, } from "./decode/motelyItemDecoder.js";
|
package/dist/motely.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Layer } from "./Layer.js";
|
|
2
|
+
export interface JamlCardRendererProps {
|
|
3
|
+
layers: Layer[];
|
|
4
|
+
invert?: boolean;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function JamlCardRenderer({ layers, invert, className }: JamlCardRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { SPRITE_SHEETS } from "../sprites/spriteData.js";
|
|
5
|
+
function loadImage(url) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const image = new window.Image();
|
|
8
|
+
image.addEventListener("load", () => {
|
|
9
|
+
resolve(image);
|
|
10
|
+
});
|
|
11
|
+
image.addEventListener("error", () => {
|
|
12
|
+
resolve(null);
|
|
13
|
+
});
|
|
14
|
+
image.src = url;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function renderImage(canvas, context, image, layer, timestamp) {
|
|
18
|
+
if (!image || !layer || !layer?.pos)
|
|
19
|
+
return 0;
|
|
20
|
+
const cardWidth = image.width / layer.columns;
|
|
21
|
+
const cardHeight = image.height / layer.rows;
|
|
22
|
+
const canvasStyle = canvas.style;
|
|
23
|
+
if (layer.order === 0) {
|
|
24
|
+
canvas.width = cardWidth;
|
|
25
|
+
canvas.height = cardHeight;
|
|
26
|
+
canvasStyle.width = `${cardWidth}px`;
|
|
27
|
+
canvasStyle.height = `${cardHeight}px`;
|
|
28
|
+
}
|
|
29
|
+
canvasStyle.imageRendering = "pixelated";
|
|
30
|
+
context.imageSmoothingEnabled = true;
|
|
31
|
+
context.save();
|
|
32
|
+
if (layer.animated && timestamp) {
|
|
33
|
+
const elapsed = timestamp;
|
|
34
|
+
const yOffset = Math.sin(elapsed / 1000) * 3;
|
|
35
|
+
const xOffset = Math.sin(elapsed / 1500) * 1.5;
|
|
36
|
+
context.globalAlpha = 0.65 + (Math.sin(elapsed / 2000) + 1) * 0.075;
|
|
37
|
+
context.translate(xOffset, yOffset);
|
|
38
|
+
}
|
|
39
|
+
context.drawImage(image, layer.pos.x * cardWidth, layer.pos.y * cardHeight, cardWidth, cardHeight, 0, 0, canvas.width, canvas.height);
|
|
40
|
+
context.restore();
|
|
41
|
+
return cardWidth / cardHeight;
|
|
42
|
+
}
|
|
43
|
+
export function JamlCardRenderer({ layers, invert = false, className = "" }) {
|
|
44
|
+
const canvasRef = useRef(null);
|
|
45
|
+
const imageCacheRef = useRef(new Map());
|
|
46
|
+
const [ratio, setRatio] = useState(3 / 4);
|
|
47
|
+
const [, forceUpdate] = useState(0);
|
|
48
|
+
const animationFrameRef = useRef(null);
|
|
49
|
+
const [elapsed, setElapsed] = useState(0);
|
|
50
|
+
const hasAnimatedLayer = layers?.some((layer) => layer.animated);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
let cancelled = false;
|
|
53
|
+
const imageCache = imageCacheRef.current;
|
|
54
|
+
const preload = async () => {
|
|
55
|
+
const urls = Array.from(new Set(Object.values(SPRITE_SHEETS).map((sheet) => sheet.src)));
|
|
56
|
+
const images = await Promise.all(urls.map((url) => loadImage(url)));
|
|
57
|
+
if (cancelled)
|
|
58
|
+
return;
|
|
59
|
+
images.forEach((image, index) => {
|
|
60
|
+
if (image) {
|
|
61
|
+
imageCache.set(urls[index], image);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
forceUpdate((prev) => prev + 1);
|
|
65
|
+
};
|
|
66
|
+
preload().catch((err) => {
|
|
67
|
+
console.error("[JamlCardRenderer]", err);
|
|
68
|
+
});
|
|
69
|
+
return () => {
|
|
70
|
+
cancelled = true;
|
|
71
|
+
imageCache.clear();
|
|
72
|
+
};
|
|
73
|
+
}, []);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!hasAnimatedLayer)
|
|
76
|
+
return;
|
|
77
|
+
let startTime;
|
|
78
|
+
const animate = (timestamp) => {
|
|
79
|
+
if (!startTime)
|
|
80
|
+
startTime = timestamp;
|
|
81
|
+
const now = timestamp - startTime;
|
|
82
|
+
if (!animationFrameRef.current || timestamp - 100 > animationFrameRef.current) {
|
|
83
|
+
animationFrameRef.current = timestamp;
|
|
84
|
+
setElapsed(now);
|
|
85
|
+
}
|
|
86
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
87
|
+
};
|
|
88
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
89
|
+
return () => {
|
|
90
|
+
if (animationFrameRef.current) {
|
|
91
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}, [hasAnimatedLayer]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!canvasRef.current || !layers || layers.length === 0)
|
|
97
|
+
return;
|
|
98
|
+
const canvas = canvasRef.current;
|
|
99
|
+
const context = canvas.getContext("2d");
|
|
100
|
+
if (!context)
|
|
101
|
+
return;
|
|
102
|
+
let cancelled = false;
|
|
103
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
104
|
+
[...layers]
|
|
105
|
+
.sort((a, b) => a.order - b.order)
|
|
106
|
+
.forEach((layer) => {
|
|
107
|
+
if (imageCacheRef.current.has(layer.source)) {
|
|
108
|
+
const image = imageCacheRef.current.get(layer.source);
|
|
109
|
+
if (!image)
|
|
110
|
+
return;
|
|
111
|
+
const imageRatio = renderImage(canvas, context, image, layer, hasAnimatedLayer ? elapsed : undefined);
|
|
112
|
+
if (layer.order === 0) {
|
|
113
|
+
setRatio(imageRatio);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
loadImage(layer.source).then((img) => {
|
|
118
|
+
if (cancelled || !img)
|
|
119
|
+
return;
|
|
120
|
+
const imageRatio = renderImage(canvas, context, img, layer, hasAnimatedLayer ? elapsed : undefined);
|
|
121
|
+
imageCacheRef.current.set(layer.source, img);
|
|
122
|
+
if (layer.order === 0) {
|
|
123
|
+
setRatio(imageRatio);
|
|
124
|
+
}
|
|
125
|
+
forceUpdate((prev) => prev + 1);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
if (invert) {
|
|
129
|
+
canvas.style.filter = "invert(0.94)";
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
canvas.style.filter = "none";
|
|
133
|
+
}
|
|
134
|
+
return () => { cancelled = true; };
|
|
135
|
+
}, [layers, elapsed, invert, hasAnimatedLayer]);
|
|
136
|
+
const containerStyle = {
|
|
137
|
+
aspectRatio: String(ratio),
|
|
138
|
+
width: "100%",
|
|
139
|
+
display: "flex",
|
|
140
|
+
};
|
|
141
|
+
const canvasStyle = {
|
|
142
|
+
borderRadius: "6px",
|
|
143
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
|
|
144
|
+
imageRendering: "pixelated",
|
|
145
|
+
};
|
|
146
|
+
return (_jsx("div", { className: className, style: containerStyle, children: _jsx("canvas", { ref: canvasRef, style: canvasStyle }) }));
|
|
147
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer class for sprite-based card rendering.
|
|
3
|
+
* Encapsulates the source, position, and dimensions of a sprite layer.
|
|
4
|
+
*/
|
|
5
|
+
export interface LayerOptions {
|
|
6
|
+
pos: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
};
|
|
10
|
+
name: string;
|
|
11
|
+
order: number;
|
|
12
|
+
source: string;
|
|
13
|
+
rows: number;
|
|
14
|
+
columns: number;
|
|
15
|
+
animated?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare class Layer {
|
|
18
|
+
pos: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
};
|
|
22
|
+
name: string;
|
|
23
|
+
order: number;
|
|
24
|
+
source: string;
|
|
25
|
+
rows: number;
|
|
26
|
+
columns: number;
|
|
27
|
+
animated: boolean;
|
|
28
|
+
constructor({ pos, name, order, source, rows, columns, animated }: LayerOptions);
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class Layer {
|
|
2
|
+
pos;
|
|
3
|
+
name;
|
|
4
|
+
order;
|
|
5
|
+
source;
|
|
6
|
+
rows;
|
|
7
|
+
columns;
|
|
8
|
+
animated;
|
|
9
|
+
constructor({ pos, name, order, source, rows, columns, animated = false }) {
|
|
10
|
+
this.pos = pos;
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.order = order;
|
|
13
|
+
this.source = source;
|
|
14
|
+
this.rows = rows;
|
|
15
|
+
this.columns = columns;
|
|
16
|
+
this.animated = animated;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprite sheet position data for all Balatro game elements.
|
|
3
|
+
* Extracted from Blueprint's const.ts — maps item names to {x, y} grid positions
|
|
4
|
+
* within their respective sprite sheets.
|
|
5
|
+
*/
|
|
6
|
+
import { type JamlAssetKey } from "../assets.js";
|
|
7
|
+
export interface SpritePos {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SpriteEntry {
|
|
12
|
+
name: string;
|
|
13
|
+
pos: SpritePos;
|
|
14
|
+
animated?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface SpriteSheetInfo {
|
|
17
|
+
readonly asset: JamlAssetKey;
|
|
18
|
+
readonly fileName: string;
|
|
19
|
+
readonly columns: number;
|
|
20
|
+
readonly rows: number;
|
|
21
|
+
readonly src: string;
|
|
22
|
+
}
|
|
23
|
+
/** Sprite sheet grid dimensions */
|
|
24
|
+
export declare const SPRITE_SHEETS: {
|
|
25
|
+
readonly jokers: SpriteSheetInfo;
|
|
26
|
+
readonly tarots: SpriteSheetInfo;
|
|
27
|
+
readonly deck: SpriteSheetInfo;
|
|
28
|
+
readonly enhancers: SpriteSheetInfo;
|
|
29
|
+
readonly editions: SpriteSheetInfo;
|
|
30
|
+
readonly stickers: SpriteSheetInfo;
|
|
31
|
+
readonly blinds: SpriteSheetInfo;
|
|
32
|
+
readonly vouchers: SpriteSheetInfo;
|
|
33
|
+
readonly tags: SpriteSheetInfo;
|
|
34
|
+
readonly boosters: SpriteSheetInfo;
|
|
35
|
+
};
|
|
36
|
+
export declare const JOKERS: SpriteEntry[];
|
|
37
|
+
export declare const JOKER_FACES: SpriteEntry[];
|
|
38
|
+
export declare const CONSUMABLE_FACES: SpriteEntry[];
|
|
39
|
+
export declare const TAROTS_AND_PLANETS: SpriteEntry[];
|
|
40
|
+
export declare const TAGS: SpriteEntry[];
|
|
41
|
+
export declare const VOUCHERS: SpriteEntry[];
|
|
42
|
+
export declare const BOSSES: SpriteEntry[];
|
|
43
|
+
export declare const BOOSTER_PACKS: SpriteEntry[];
|
|
44
|
+
export declare const EDITION_MAP: Record<string, number>;
|
|
45
|
+
export declare const STICKER_MAP: Record<string, SpritePos>;
|
|
46
|
+
export declare const RANK_MAP: Record<string, number>;
|
|
47
|
+
export declare const SUIT_MAP: Record<string, number>;
|
|
48
|
+
export declare const ENHANCER_MAP: Record<string, SpritePos>;
|
|
49
|
+
export declare const SEAL_MAP: Record<string, SpritePos>;
|
|
50
|
+
export declare function findJoker(name: string): SpriteEntry | undefined;
|
|
51
|
+
export declare function findJokerFace(name: string): SpriteEntry | undefined;
|
|
52
|
+
export declare function findConsumable(name: string): SpriteEntry | undefined;
|
|
53
|
+
export declare function findConsumableFace(name: string): SpriteEntry | undefined;
|
|
54
|
+
export declare function findTag(name: string): SpriteEntry | undefined;
|
|
55
|
+
export declare function findVoucher(name: string): SpriteEntry | undefined;
|
|
56
|
+
export declare function findBoss(name: string): SpriteEntry | undefined;
|
|
57
|
+
export declare function findBooster(name: string): SpriteEntry | undefined;
|