Improved card design.
This commit is contained in:
@@ -3,6 +3,60 @@ import { createPortal } from 'react-dom'
|
||||
import type { Card } from '../types'
|
||||
import { artFor } from '../petArt'
|
||||
|
||||
// The suit's "hat" — the trade-in symbol printed on the real cards, tinted to
|
||||
// the suit's colour. Drawn as a tiny sun-hat SVG so it scales crisply.
|
||||
const SUIT_HAT: Record<string, { fill: string; band: string }> = {
|
||||
red: { fill: '#e5563f', band: '#b62c1f' },
|
||||
blue: { fill: '#4d8ce3', band: '#2b5fb0' },
|
||||
yellow: { fill: '#f2c24d', band: '#d9992f' },
|
||||
}
|
||||
|
||||
function SuitHat({ suit }: { suit: string }) {
|
||||
const c = SUIT_HAT[suit] ?? SUIT_HAT.yellow
|
||||
return (
|
||||
<svg className="card-hat" viewBox="0 0 28 18" aria-hidden>
|
||||
<path
|
||||
d="M7 12.5 Q7 3.5 14 3.5 Q21 3.5 21 12.5 Z"
|
||||
fill={c.fill}
|
||||
stroke="rgba(50,25,8,.45)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<path d="M7.4 11 Q14 13 20.6 11 L20.6 12.6 Q14 14.6 7.4 12.6 Z" fill={c.band} />
|
||||
<ellipse
|
||||
cx="14"
|
||||
cy="13.4"
|
||||
rx="12.6"
|
||||
ry="3.7"
|
||||
fill={c.fill}
|
||||
stroke="rgba(50,25,8,.45)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// The tier shown as a die face (1–6 pips), like the real cards' corner marker.
|
||||
const DIE_PIPS: Record<number, [number, number][]> = {
|
||||
1: [[15, 15]],
|
||||
2: [[9, 9], [21, 21]],
|
||||
3: [[9, 9], [15, 15], [21, 21]],
|
||||
4: [[9, 9], [21, 9], [9, 21], [21, 21]],
|
||||
5: [[9, 9], [21, 9], [15, 15], [9, 21], [21, 21]],
|
||||
6: [[9, 9], [21, 9], [9, 15], [21, 15], [9, 21], [21, 21]],
|
||||
}
|
||||
|
||||
function TierDie({ tier }: { tier: number }) {
|
||||
const pips = DIE_PIPS[tier] ?? DIE_PIPS[1]
|
||||
return (
|
||||
<svg className="card-die" viewBox="0 0 30 30" aria-hidden>
|
||||
<rect x="1.5" y="1.5" width="27" height="27" rx="6.5" fill="#fff" stroke="#2c1c0e" strokeWidth="2" />
|
||||
{pips.map(([cx, cy], i) => (
|
||||
<circle key={i} cx={cx} cy={cy} r="2.9" fill="#2c1c0e" />
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// useFitText shrinks the effect text until it fits its band, so long abilities
|
||||
// (and the smaller battle cards) render in full instead of being clipped by
|
||||
// the fixed card height. It never grows past the CSS size, only shrinks toward
|
||||
@@ -177,6 +231,9 @@ export function CardView({
|
||||
// Shrink long ability text to fit the (fixed-height) card, re-fitting when
|
||||
// the text or card size changes. Only the branch that renders attaches it.
|
||||
const effectRef = useFitText(`${size}|${card.effectText ?? ''}|${card.food ?? ''}`)
|
||||
// Long single-word names (e.g. Calygreyhound) shrink to fit the name row
|
||||
// instead of wrapping into a ragged stack.
|
||||
const nameRef = useFitText(`${size}|${card.name}`)
|
||||
const classes = [
|
||||
'card',
|
||||
`card-${size}`,
|
||||
@@ -206,33 +263,43 @@ export function CardView({
|
||||
onMouseLeave={preview ? undefined : () => setHover(null)}
|
||||
>
|
||||
{previewEl}
|
||||
<div className="card-top">
|
||||
<span className="card-tier">{card.tier ? `T${card.tier}` : '·'}</span>
|
||||
{card.suit && <span className={`suit-dot suit-${card.suit}`} title={`${card.suit} suit`} />}
|
||||
</div>
|
||||
{/* Power sits in a spiky burst badge on its own centered row (pets only),
|
||||
with any battle damage marker beside it, so it never overlaps the art,
|
||||
name, or ability text. */}
|
||||
{card.kind === 'pet' && (
|
||||
<div className="card-combat">
|
||||
{/* Upper illustration: a stylised landscape with the pet as a sticker and
|
||||
the red starburst attack badge floating at the top, like the real
|
||||
cards. */}
|
||||
<div className="card-scene">
|
||||
{card.kind === 'pet' && (
|
||||
<span className={`card-power ${bonus > 0 ? 'is-buffed' : ''}`}>{power}</span>
|
||||
{damage > 0 && !dead && <span className="card-damage">−{damage}</span>}
|
||||
)}
|
||||
{damage > 0 && !dead && <span className="card-damage">−{damage}</span>}
|
||||
<div className="card-art" aria-hidden>
|
||||
{artFor(card.name)}
|
||||
</div>
|
||||
)}
|
||||
<div className="card-art" aria-hidden>
|
||||
{artFor(card.name)}
|
||||
</div>
|
||||
<div className="card-name">{card.name}</div>
|
||||
<div className="card-effect" ref={effectRef}>
|
||||
{card.effectText
|
||||
? renderEffect(card.effectText)
|
||||
: card.kind === 'food' && card.food === 'apple'
|
||||
? '+1 power (this battle)'
|
||||
: card.ailment === 'spooked'
|
||||
? 'Deals 1 less damage'
|
||||
: card.ailment === 'exposed'
|
||||
? 'Takes 1 extra damage'
|
||||
: ''}
|
||||
{/* Lower panel: the suit hat, name, and tier die on one row, the ability
|
||||
beneath — a white card in a rounded green frame. */}
|
||||
<div className="card-panel">
|
||||
<div className="card-panel-head">
|
||||
{card.suit ? (
|
||||
<SuitHat suit={card.suit} />
|
||||
) : (
|
||||
<span className="card-panel-slot" aria-hidden />
|
||||
)}
|
||||
<span className="card-name" ref={nameRef}>
|
||||
{card.name}
|
||||
</span>
|
||||
{card.tier ? <TierDie tier={card.tier} /> : <span className="card-panel-slot" aria-hidden />}
|
||||
</div>
|
||||
<div className="card-effect" ref={effectRef}>
|
||||
{card.effectText
|
||||
? renderEffect(card.effectText)
|
||||
: card.kind === 'food' && card.food === 'apple'
|
||||
? '+1 power (this battle)'
|
||||
: card.ailment === 'spooked'
|
||||
? 'Deals 1 less damage'
|
||||
: card.ailment === 'exposed'
|
||||
? 'Takes 1 extra damage'
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
{selected && <div className="card-check">✓</div>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user