Last updated: 2026-02-08
Status: In Progress — Core implementation complete, iterating on UX and asset pack persistence.
- types.ts —
DeckList,EnrichedCard, validation/stats types,enrichCard()utility - validation.ts —
validateDeck(),canAddCard()enforcing One Piece TCG tournament rules (50-card main, 10-card DON, 1 leader, max 4 copies) - stats.ts —
calculateDeckStats()for cost curve, color distribution, type breakdown, counter values - serialization.ts — YAML/TOML import/export with
downloadFile()andreadFileAsText() - storage.ts — IndexedDB persistence for saved decks via
idb-keyval - index.ts — Public API barrel exports
- local-loader.ts (NEW) —
loadLocalZip(),loadLocalDirectory(),reloadLocalPack()with fflate zip creation and extraction - types.ts — Added
LocalSourcetoAssetPackSourceunion; addedcards?andmanifest?fields toStoredPackMetadatafor fast reload - loader.ts — Added local source dispatch in
loadPack()with dual-cache lookup (loader + local-loader maps) and IndexedDB reconstruction - cache.ts — Three-store IndexedDB architecture (see Architecture Decisions below)
- index.ts — Exports for
reloadLocalPack, zip extractor utilities
- useDeckBuilder.ts — Deck state management with undo/redo history
- useDeckStorage.ts — IndexedDB CRUD wrapper for saved decks
- useDeckValidation.ts — Reactive validation with error/warning status
- DeckBuilderPage.tsx — Main orchestration: multi-pack support, tab-based right panel (Packs/Deck/Stats/I-O/Saved)
- CardBrowser.tsx — Card grid with size options (S/M/L), hover preview pane showing larger card + metadata,
cardPackMapfor multi-pack image routing - CardFilters.tsx — Filter controls (color, type, cost, search text)
- CardPreview.tsx — Card detail display (accepts nullable packId)
- CardTile.tsx — Individual card tile with
aspectRatio: '5/7'andobjectFit: 'contain' - DeckListPanel.tsx — Deck list grouped by type, uses
cardPackMapfor image resolution - DeckStats.tsx — Statistics panel with cost curve and color/type breakdowns
- DeckValidation.tsx — Validation status bar with error/warning counts
- DeckManager.tsx — Saved decks list with load/duplicate/delete
- ImportExportPanel.tsx — YAML/TOML import/export controls
- AssetPackUpload.tsx — Upload zip or select directory with progress bar
- App.tsx — Added
deck-buildergame mode routing - GameSelector.tsx — Added "Deck Builder" button
- bracket-styles.ts — Configurable regex pattern→hex color system for highlighting
[Keyword]text in card effects. localStorage persistence followingassets/config.tspattern. Compiled regex cache for performance. - StyledEffectText.tsx — React component that parses effect text and renders bracket keywords as colored
<span>elements withuseMemocaching. - 15 default One Piece TCG keyword patterns: On Play, When Attacking, Trigger, DON.* (regex), Blocker, Rush, Counter, Activate:\s*Main, Main, etc.
- Integrated into both
HoverPreviewPane(CardBrowser.tsx) and detail modal (CardPreview.tsx)
- Preview pane width doubled to 400px (2× Large grid size) for better card visibility
- All preview text font sizes doubled for readability
- Card name: 28px, tags: 20px, info lines: 22px, effect text: 22px
deck/types.test.ts— 7 tests (enrichCard)deck/validation.test.ts— 14 tests (validateDeck, canAddCard)deck/stats.test.ts— 10 tests (calculateDeckStats)deck/serialization.test.ts— 13 tests (YAML/TOML round-trip, validation)deck/storage.test.ts— 8 tests (IndexedDB CRUD with mocked idb-keyval)deck/bracket-styles.test.ts— 16 tests (parser, regex matching, fallback colors, custom configs, case insensitivity)
Each data type gets its own IndexedDB database to prevent namespace collisions:
| Store | Database Name | Key | Value |
|---|---|---|---|
| Pack metadata | manamesh-asset-packs |
packId |
StoredPackMetadata |
| Card images | manamesh-card-images |
pack:{id}:card:{id}:{side} |
Blob |
| Zip archives | manamesh-pack-zips |
packId |
Blob |
Why: An earlier design stored zip blobs in the metadata store with a zip: key prefix. getAllPackMetadata() then returned zip Blobs alongside metadata objects, causing phantom entries in the UI. Separate stores eliminate this class of bug entirely.
- Fast path:
StoredPackMetadatanow includes optionalcardsandmanifestfields. On reload, read metadata from IndexedDB — single key-value lookup, no decompression needed. - Slow path (fallback): For packs stored before the
cardsfield was added, extract from the stored zip blob, parse manifest, resolve nested sets. After extraction, backfill the metadata withcards/manifestso subsequent reloads use the fast path.
Instead of a single packId: string prop, components receive cardPackMap: Map<string, string> (cardId -> packId). This allows cards from multiple loaded packs to coexist in the browser, with each card's images routed to the correct pack's cache.
- User selects directory via
<input webkitdirectory> - All files read into
Map<string, Uint8Array> - Zipped client-side with
fflate - Zip blob stored in
manamesh-pack-zipsIndexedDB store - Extracted, manifest parsed, nested set manifests resolved
- Card images cached individually in
manamesh-card-images - Pack metadata (including full card entries) stored in
manamesh-asset-packs - On next visit, pack appears in "Available Packs" — reload reads metadata directly (fast path) or re-extracts from zip (slow path)
- Cause: Zip blobs stored in the same IndexedDB store as pack metadata with a
zip:key prefix.getAllPackMetadata()returned both, and zip Blobs rendered as empty metadata entries. - Fix: Moved zip blobs to dedicated
manamesh-pack-zipsstore.getAllPackMetadata()now scans a clean store.
- Cause: Dual in-memory cache.
loader.tsandlocal-loader.tseach maintain separateMap<string, LoadedAssetPack>instances that don't know about each other.loadPack()for local sources only checkedloader.ts's map. - Fix:
loadPack()now checks local-loader's cache viagetLocalPack(), then attempts IndexedDB reconstruction viareloadLocalPack().
- Cause:
objectFit: 'cover'with fixed pixel heights on<img>elements crops card art to fill the tile. - Fix: Changed to
objectFit: 'contain'withaspectRatio: '5/7'(standard card proportions ~63x88mm) on tile containers,width: 100%; height: 100%on images.
- Cause: CSS Grid as a direct flex child with
flex: 1distributes its constrained height across all rows instead of allowing overflow. Cards get squished into tiny slices. - Fix: Separated scroll container from grid container — outer div gets flex sizing (
flex: 1, minHeight: 0, overflowY: auto), inner div is the unconstrained grid that overflows naturally.
- Cause: The commit-reveal seed steps in Go Fish's shuffle phase were gated behind
isMySetupTurnin GoFishBoard.tsx. Only the sequentialshuffleDeckcall should be gated — commit/reveal must be open to all players simultaneously. - Fix: Removed
isMySetupTurngate from commit/reveal steps; addedG.shuffleRngto useEffect dependency array.
js-yaml+@types/js-yaml— YAML serialization for deck import/exportsmol-toml— TOML serialization for deck import/exportfflate— Already present; now also used for zip creation (directory -> zip) in addition to extraction
- Vite build: PASS (8.8MB single-file output)
- Tests: 1052 passed, 3 skipped (58 test files)
- Type-check: Pre-existing errors in third-party packages only; project code is clean
- Users upgrading from before the three-store refactor need to clear IndexedDB manually (or use "Clear All Stored Packs" button) to remove stale data from the old schema
- IPFS/HTTP asset pack loading is stubbed but not yet tested end-to-end
- Card preview hover pane could show card back on hover-over-flip interaction
- No "delete single stored pack" button yet (only "Clear All")
- Deck builder now integrates with the game lobby via the ready phase (MM-040)
Status: In Progress — All 8 user stories implemented. Lobby ready phase complete. Pending manual E2E testing.
chunkBlob()— splits Blob into base64-encoded chunks (48KB raw → ~64KB base64)createChunkCollector()— stateful reassembly with progress callbacks, out-of-order & duplicate handlingarrayBufferToBase64()/base64ToArrayBuffer()— browser-native conversion utilities- 12 tests covering round-trips, multi-chunk splits, progress, edge cases
- 8 message types:
deck-list-share,deck-list-ack,asset-pack-request,asset-pack-offer,asset-pack-chunk,asset-pack-complete,asset-pack-denied,asset-pack-cancel PeerBlockList— per-player session-scoped blocking with auto-block after N requestsAssetSharingSession— transfer state tracking + static message factory methodsfindMissingCards(),isAssetSharingMessage()utilities- 25 tests covering block list, auto-blocking, transfer tracking, message factories
- JSON envelope
{ _lobby: true, payload }over rawJoinCodeConnection.send(string) - Supports both
AssetSharingMessage(deck list, asset transfer) andLobbyControlMessage(ready state) handleRawMessage()returns true if consumed, false for passthrough to other handlers- 11 tests covering envelope format, routing, control messages, subscribe/unsubscribe
buildCardsOnlyBlob()— reads card images from IndexedDB, packages with length-prefixed JSON headerbuildFullPackBlob()— reads zip archive from IndexedDBsendCardsTransfer()/sendFullPackTransfer()— build blob → chunk → send offer + chunks + completeunpackCardsOnlyBlob()— parse header, extract images, store in IndexedDB with pack metadataunpackFullPackBlob()— store zip blob for later loading- Binary format:
[4-byte uint32 BE header length][JSON header][concatenated image blobs] - 8 tests covering build, round-trip, progress callbacks, metadata preservation
- Extended
P2PMessageTypeunion with 8 asset sharing types onAssetSharingMessage(cb),sendAssetSharingMessage(msg),handleAssetSharingMessage()
types.ts—P2PSourceinterface, extendedAssetPackSourceunionloader.ts— P2P source handler (reconstructs from IndexedDB metadata)
ConsentDialog.tsx—SenderConsentDialog,ReceiverConsentDialog,MissingPacksNoticeTransferProgress.tsx— Progress bar with chunk counts, cancel button, status colorsBlockList.tsx— Blocked peers list with unblock buttons
- Bridges transport layer → protocol state machine → React state
- Real transfer pipeline:
allowSenderRequestbuilds blob, chunks, and sends - Incoming chunks reassembled via
createChunkCollector, unpacked viaunpackCardsOnlyBlob/unpackFullPackBlob
- Connection no longer immediately starts game — enters deck selection phase
ReadyPhaseUIcomponent: deck selector, deck list sharing, ready coordination- Saved decks listed via
useDeckStorage()hook - Deck list shared with peer on selection via
AssetSharingSession.createDeckListShare() lobby-readycontrol message sent on Ready/Cancel Ready toggle- Game starts only when both players are ready (800ms UI delay for feedback)
- Integrated asset sharing overlays (missing packs, transfer progress, consent dialogs, block list)
AssetPackUpload.tsx— IPFS CID input with CIDv0/CIDv1 validationDeckBuilderPage.tsx— CID display + copy foripfs:/ipfs-zip:loaded packs
- Standalone page accessible from GameSelector ("Asset Packs" button)
- IPFS CID import field with validation and progress
- Stored packs list with card count, source info, IPFS CID display
- Per-pack delete with confirmation dialog
App.tsxroute:"asset-packs"game modeGameSelector.tsx— "Asset Packs" button (purple) next to "Deck Builder"
- Build: Clean (Vite, 8.8MB)
- Tests: 1052 passed, 3 skipped (58 files)
- End-to-end manual testing with two peers