Initial commit

This commit is contained in:
2026-03-13 16:01:39 -04:00
commit 27e8c9a6fe
22 changed files with 8681 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
import React from 'react';
import CardArt from './CardArt';
const COLOR_MAP = {
radiance: { bg: '#fff8e7', border: '#d4a843', accent: '#f0d060' },
tide: { bg: '#e7f0ff', border: '#4477bb', accent: '#6699dd' },
shadow: { bg: '#2a2a2a', border: '#666', accent: '#8a7090', text: '#ddd' },
flame: { bg: '#fff0e7', border: '#cc4422', accent: '#ee6644' },
growth: { bg: '#eaf5e7', border: '#448833', accent: '#66aa55' },
colorless: { bg: '#f0f0f0', border: '#999', accent: '#bbb' },
};
const KEYWORD_DISPLAY = {
swift: 'Swift',
vigilant: 'Vigilant',
soaring: 'Soaring',
guardian: 'Guardian',
fortified: 'Fortified',
draining: 'Draining',
overwhelming: 'Overwhelming',
venomous: 'Venomous',
reaching: 'Reaching',
};
function formatManaCost(cost) {
if (!cost) return '';
const parts = [];
if (cost.colorless) parts.push(String(cost.colorless));
const symbols = { radiance: '\u2600', tide: '\u{1F30A}', shadow: '\u{1F480}', flame: '\u{1F525}', growth: '\u{1F33F}' };
for (const [color, sym] of Object.entries(symbols)) {
for (let i = 0; i < (cost[color] || 0); i++) parts.push(sym);
}
return parts.join(' ');
}
export default function Card({
card,
instance,
onClick,
selected,
selectable,
small,
faceDown,
zone,
}) {
if (faceDown) {
return (
<div className="card card-facedown" onClick={onClick}>
<div className="card-back">Arcane Duels</div>
</div>
);
}
const data = instance?.cardData || card;
if (!data) return null;
const colors = COLOR_MAP[data.color] || COLOR_MAP.colorless;
const isCreature = data.type === 'creature';
const isLand = data.type === 'land';
const tapped = instance?.tapped;
const keywords = data.keywords || [];
const power = instance ? instance.effectivePower : data.power;
const toughness = instance ? (instance.effectiveToughness - (instance.damage || 0)) : data.toughness;
const cardClass = [
'card',
`card-${data.type}`,
`color-${data.color}`,
tapped ? 'tapped' : '',
selected ? 'selected' : '',
selectable ? 'selectable' : '',
small ? 'card-small' : '',
zone ? `zone-${zone}` : '',
instance?.summoningSickness ? 'summoning-sick' : '',
].filter(Boolean).join(' ');
const style = {
'--card-bg': colors.bg,
'--card-border': colors.border,
'--card-accent': colors.accent,
color: colors.text || '#222',
};
return (
<div className={cardClass} style={style} onClick={onClick}>
<div className="card-header">
<span className="card-name">{data.name}</span>
{!isLand && <span className="card-cost">{formatManaCost(data.cost)}</span>}
</div>
<div className="card-art">
<CardArt
cardName={data.name}
color={data.color}
type={data.type}
width={small ? 100 : 130}
height={small ? 45 : 70}
/>
</div>
<div className="card-type-line">
{data.type.charAt(0).toUpperCase() + data.type.slice(1)}
{data.subtype ? ` \u2014 ${data.subtype}` : ''}
</div>
<div className="card-text">
{keywords.length > 0 && (
<div className="card-keywords">
{keywords.map((k) => KEYWORD_DISPLAY[k] || k).join(', ')}
</div>
)}
{data.flavor && !small && (
<div className="card-flavor">{data.flavor}</div>
)}
</div>
{isCreature && (
<div className="card-pt">
<span className={instance?.damage > 0 ? 'damaged' : ''}>
{power}/{toughness}
</span>
</div>
)}
</div>
);
}
export { formatManaCost, COLOR_MAP };