Files
website/src/lib/tcg/deckBuilder.svelte.ts
T

148 lines
3.7 KiB
TypeScript

import type { DeckCategory } from '#lib/content';
import { cardCategory, categoryOrder, categoryTitles, copyLimit, deckSizeLimit, isBasicEnergy } from './cardKind';
import type { TcgCard } from './types';
export interface BuilderCard {
id: string;
name: string;
supertype?: string;
subtypes?: string[];
image: string;
setCode?: string;
number?: string;
}
export interface BuilderEntry {
card: BuilderCard;
count: number;
}
export interface BuilderSection {
category: DeckCategory;
title: string;
total: number;
entries: BuilderEntry[];
}
const storageKey = 'tcg-deck-builder';
const defaultDeckName = 'my deck';
export function toBuilderCard(card: TcgCard): BuilderCard {
return {
id: card.id,
name: card.name,
supertype: card.supertype,
subtypes: card.subtypes,
image: card.images.small,
setCode: card.set.ptcgoCode,
number: card.number
};
}
class DeckBuilderState {
name = $state(defaultDeckName);
entries = $state<BuilderEntry[]>([]);
private loaded = false;
total = $derived(this.entries.reduce((sum, entry) => sum + entry.count, 0));
sections = $derived<BuilderSection[]>(
categoryOrder
.map((category) => {
const entries = this.entries.filter((entry) => cardCategory(entry.card) === category);
return {
category,
title: categoryTitles[category],
total: entries.reduce((sum, entry) => sum + entry.count, 0),
entries
};
})
.filter((section) => section.entries.length > 0)
);
load() {
if (this.loaded) return;
this.loaded = true;
try {
const stored = JSON.parse(localStorage.getItem(storageKey) ?? 'null');
if (!stored) return;
this.name = typeof stored.name === 'string' ? stored.name : defaultDeckName;
this.entries = Array.isArray(stored.entries) ? stored.entries : [];
} catch {
return;
}
}
private save() {
try {
localStorage.setItem(storageKey, JSON.stringify({ name: this.name, entries: this.entries }));
} catch {
return;
}
}
countOf(cardId: string): number {
return this.entries.find((entry) => entry.card.id === cardId)?.count ?? 0;
}
private copiesNamed(name: string): number {
return this.entries.filter((entry) => entry.card.name === name).reduce((sum, entry) => sum + entry.count, 0);
}
canAdd(card: Pick<TcgCard, 'name' | 'supertype' | 'subtypes'>): boolean {
if (this.total >= deckSizeLimit) return false;
return isBasicEnergy(card) || this.copiesNamed(card.name) < copyLimit;
}
add(card: TcgCard) {
if (!this.canAdd(card)) return;
const entry = this.entries.find((item) => item.card.id === card.id);
if (entry) entry.count += 1;
else this.entries.push({ card: toBuilderCard(card), count: 1 });
this.save();
}
increase(cardId: string) {
const entry = this.entries.find((item) => item.card.id === cardId);
if (!entry || !this.canAdd(entry.card)) return;
entry.count += 1;
this.save();
}
remove(cardId: string) {
const entry = this.entries.find((item) => item.card.id === cardId);
if (!entry) return;
entry.count -= 1;
if (entry.count <= 0) this.entries = this.entries.filter((item) => item.card.id !== cardId);
this.save();
}
rename(name: string) {
this.name = name;
this.save();
}
replace(name: string, entries: BuilderEntry[]) {
this.name = name;
this.entries = entries;
this.save();
}
clear() {
this.entries = [];
this.save();
}
exportText(): string {
const lines = this.sections.flatMap((section) => [
`${section.title}: ${section.total}`,
...section.entries.map((entry) => [entry.count, entry.card.name, entry.card.setCode, entry.card.number].filter(Boolean).join(' ')),
''
]);
return [...lines, `Total Cards: ${this.total}`].join('\n');
}
}
export const deckBuilder = new DeckBuilderState();