148 lines
3.7 KiB
TypeScript
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();
|