import React from 'react'; import type { Player } from '../types'; export type PieceType = 'knight' | 'warrior' | 'wizard'; // Hex color parser and manipulator function parseHex(hex: string): [number, number, number] { let c = hex.replace('#', '').trim(); if (c.length === 3) { c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2]; } const num = parseInt(c, 16); if (isNaN(num)) { return [56, 189, 248]; // default sky blue } return [(num >> 16) & 255, (num >> 8) & 255, num & 255]; } function rgbToHex(r: number, g: number, b: number): string { const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v))); return '#' + [clamp(r), clamp(g), clamp(b)].map((x) => x.toString(16).padStart(2, '0')).join(''); } function adjustColor(hex: string, percent: number): string { const [r, g, b] = parseHex(hex); if (percent > 0) { return rgbToHex( r + (255 - r) * percent, g + (255 - g) * percent, b + (255 - b) * percent ); } else { const factor = 1 + percent; return rgbToHex(r * factor, g * factor, b * factor); } } // 16x16 Pixel Sprite Matrices // Characters: // . : transparent // _ : miniature drop shadow // K : dark iron armor outline (#0f172a) // C : faction color main // L : faction color light (+35%) // D : faction color dark (-30%) // M : polished plate steel (#94a3b8) // m : steel highlight (#e2e8f0) // S : steel shadow (#475569) // G : gold main (#eab308) // g : gold highlight (#fef08a) // d : gold shadow (#a16207) // W : white shine/glint (#ffffff) // H : brown wood/leather haft (#78350f) // B : miniature pedestal top (#334155) // b : miniature pedestal rim (#1e293b) // R : ruby gem or glowing battle eye (#ef4444) const KNIGHT_SPRITE: string[] = [ '......LCD.......', // Row 0: Plume crest tip in faction color '.....LCCD.......', // Row 1: Plume feather body '....KmMSK.......', // Row 2: Steel knight helm apex '...KmMSSSK......', // Row 3: Helm brow '...KMKWKSK...mW.', // Row 4: Eye slit gleam + Sword tip '...KSKKSSK...Mm.', // Row 5: Lower helm + Sword blade '.LCKMmMSKK...Mm.', // Row 6: Paired pauldron + breastplate + blade 'LCCKmSSKGGGGGMm.', // Row 7: Shield top in color + sword crossguard 'LCCKMSKK.KHK....', // Row 8: Shield face + sword hilt 'DCDKMSKK.KGK....', // Row 9: Shield face + golden pommel '.DDK.SS.K.......', // Row 10: Shield tip + armored legs '..K..SS..K......', // Row 11: Iron sabatons '...KKKKKKKKK....', // Row 12: Pedestal top bevel '..KBBBBBBBBBBK..', // Row 13: Pedestal stone base '.KbbbbbbbbbbbbK.', // Row 14: Pedestal stone bevel rim '..____________..', // Row 15: Miniature drop shadow ]; const LEADER_KNIGHT_SPRITE: string[] = [ '....g.g.g.......', // Row 0: 3-point Golden Royal Crown '....GgGgG.......', // Row 1: Crown band with jewels '....KGRGK.......', // Row 2: Crown base with Ruby gem '...KmMSSSK......', // Row 3: Knight visor brow '...KMKWKSK...mW.', // Row 4: Eye slit gleam + Sword tip '...KSKKSSK...Mm.', // Row 5: Lower helm + Sword blade '.GGKMmMSKK...Mm.', // Row 6: Golden cape clasps + breastplate + blade 'GLCKmSSKGGGGGMm.', // Row 7: Gold-trimmed Shield + sword crossguard 'GCCKMSKK.KHK....', // Row 8: Shield in faction color + sword hilt 'GDDKMSKK.KGK....', // Row 9: Shield in faction color + gold pommel '.GGK.SS.K.......', // Row 10: Golden shield tip + armored legs '..K..SS..K......', // Row 11: Iron sabatons '...KGGGGGGGK....', // Row 12: Golden pedestal top '..KBBBBBBBBBBK..', // Row 13: Pedestal stone base '.KGGGGGGGGGGGGK.', // Row 14: Golden pedestal rim '..____________..', // Row 15: Miniature drop shadow ]; const WARRIOR_SPRITE: string[] = [ '.mW..........Wm.', // Row 0: Curved battle horns '..Mm...KK...mM..', // Row 1: Horn bodies + helmet crest '...MKKmMSKKM....', // Row 2: Horn bases on Spiked Iron Helm '.Wm.KMMSSK.mW...', // Row 3: Dual battle-axe blades + brow 'WmM.KKRRKK.MmW..', // Row 4: Axe blades + Fierce red eyes 'WMMMKSSSKMMMW...', // Row 5: Full axe blades + lower iron plate '.KMMKMmMSKMMK...', // Row 6: Axe blade curves + iron breastplate '..KKKKHHKKKK....', // Row 7: Axe haft collar + spiked pauldrons '...KLCCCCDK.....', // Row 8: Warrior war-tunic in faction color '...KCCCCCCK.....', // Row 9: Faction color tunic '...KGKKKKGK.....', // Row 10: Spiked warrior war belt with gold rivets '...KHS..SHK.....', // Row 11: Leather greaves & combat boots '...KKKKKKKKK....', // Row 12: Pedestal top bevel '..KBBBBBBBBBBK..', // Row 13: Pedestal stone base '.KbbbbbbbbbbbbK.', // Row 14: Pedestal stone bevel rim '..____________..', // Row 15: Miniature drop shadow ]; const LEADER_WARRIOR_SPRITE: string[] = [ '.g.g.g....g.g.g.', // Row 0: Golden crown horn tips '..GgG..KK..GgG..', // Row 1: Golden horn bodies '...GKKmMSKKG....', // Row 2: Golden horn bases on helmet '.Wm.KMMSSK.mW...', // Row 3: Battle-axe blades + brow 'WmM.KKRRKK.MmW..', // Row 4: Axe blades + Fierce red eyes 'WMMMKSSSKMMMW...', // Row 5: Axe blades + lower helm '.KMMKMmMSKMMK...', // Row 6: Axe blade curves + breastplate '..KKKKHHKKKK....', // Row 7: Pauldrons + haft collar '...KLCCCCDK.....', // Row 8: Tunic in faction color '...KCCCCCCK.....', // Row 9: Faction color tunic '...GGGGGGGG.....', // Row 10: Golden warrior war belt '...KHS..SHK.....', // Row 11: Leather boots '...KGGGGGGGK....', // Row 12: Golden pedestal top '..KBBBBBBBBBBK..', // Row 13: Pedestal stone base '.KGGGGGGGGGGGGK.', // Row 14: Golden pedestal rim '..____________..', // Row 15: Miniature drop shadow ]; const WIZARD_SPRITE: string[] = [ '......CC........', // Row 0: Pointed wizard hat tip '.....LCCD.......', // Row 1: Hat cone '....LLCCDD......', // Row 2: Hat cone body '...GLLCCDDG.gW..', // Row 3: Golden hat buckle + Staff crystal shine '..KCCCCCCCCK.Gg.', // Row 4: Wide hat brim + Staff orb '...KWWWWWWK..KH.', // Row 5: Face & white flowing beard + wood staff '...KWBRBWK...KH.', // Row 6: Glowing mystic eyes + beard + staff '.LCKWWWWWWK..KH.', // Row 7: Robe shoulders + beard + staff 'LCCKLLCCDDG..KH.', // Row 8: Robe body with golden clasp + staff 'DCDKLLCCDDK..KH.', // Row 9: Flowing robe + staff '.DDKLLCCDDK..KH.', // Row 10: Robe lower + staff '..K.KBBBBK.K.KH.', // Row 11: Mystic boots + staff base '...KKKKKKKKK....', // Row 12: Pedestal top bevel '..KBBBBBBBBBBK..', // Row 13: Pedestal stone base '.KCCCCCCCCCCCCK.', // Row 14: Pedestal rim in robe color '..____________..', // Row 15: Miniature drop shadow ]; // Palette generation function getPalette(color: string): Record { const cMain = color.trim().startsWith('#') ? color.trim() : `#${color.trim()}`; const cLight = adjustColor(cMain, 0.40); const cDark = adjustColor(cMain, -0.32); return { '.': '', // transparent '_': 'rgba(0, 0, 0, 0.45)', // drop shadow 'K': '#0f172a', // dark outline 'C': cMain, 'L': cLight, 'D': cDark, 'M': '#94a3b8', // plate steel 'm': '#e2e8f0', // steel highlight 'S': '#475569', // steel shadow 'G': '#eab308', // gold main 'g': '#fef08a', // gold light 'd': '#a16207', // gold dark 'W': '#ffffff', // white glint 'H': '#78350f', // wood / leather 'h': '#451a03', // wood dark 'B': '#334155', // pedestal stone 'b': '#1e293b', // pedestal rim 'R': '#ef4444', // ruby / battle eyes }; } // In-memory cache for pre-rendered 16x16 canvases and data URLs const spriteCanvasCache = new Map(); const spriteDataUrlCache = new Map(); export function getPlayerPieceType(player: { name: string; id?: string; piece_type?: string; is_party_leader?: boolean; party_id?: string | null; }): PieceType { // Party leaders are always Knights commanding the squad if (player.is_party_leader) { return 'knight'; } // Party squad followers are loyal Warriors if (player.party_id && !player.is_party_leader) { return 'warrior'; } // Explicit piece type if present if (player.piece_type === 'warrior' || player.piece_type === 'knight') { return player.piece_type; } // Check name indicators const lower = player.name.toLowerCase(); if ( lower.includes('knight') || lower.includes('tank') || lower.includes('titan') || lower.includes('paladin') || lower.includes('gear') || lower.includes('iron') || lower.includes('guard') ) { return 'knight'; } if ( lower.includes('warrior') || lower.includes('viper') || lower.includes('striker') || lower.includes('scout') || lower.includes('ranger') || lower.includes('axe') || lower.includes('blade') ) { return 'warrior'; } // Deterministic stable hash for variety let hash = 0; const str = player.id || player.name; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; } return Math.abs(hash) % 2 === 0 ? 'knight' : 'warrior'; } export function getSpriteCanvas( pieceType: PieceType, color: string, isLeader: boolean = false ): HTMLCanvasElement { const normColor = (color || '#38bdf8').toLowerCase(); const cacheKey = `${pieceType}_${normColor}_${isLeader ? '1' : '0'}`; const cached = spriteCanvasCache.get(cacheKey); if (cached) { return cached; } const canvas = document.createElement('canvas'); canvas.width = 16; canvas.height = 16; const ctx = canvas.getContext('2d'); if (ctx) { let spriteMatrix: string[]; if (pieceType === 'knight') { spriteMatrix = isLeader ? LEADER_KNIGHT_SPRITE : KNIGHT_SPRITE; } else if (pieceType === 'wizard') { spriteMatrix = WIZARD_SPRITE; } else { spriteMatrix = isLeader ? LEADER_WARRIOR_SPRITE : WARRIOR_SPRITE; } const palette = getPalette(normColor); for (let y = 0; y < 16; y++) { const row = spriteMatrix[y] || '................'; for (let x = 0; x < 16; x++) { const char = row[x] || '.'; const col = palette[char]; if (col) { ctx.fillStyle = col; ctx.fillRect(x, y, 1, 1); } } } } spriteCanvasCache.set(cacheKey, canvas); return canvas; } export function getSpriteDataUrl( pieceType: PieceType, color: string, isLeader: boolean = false ): string { const normColor = (color || '#38bdf8').toLowerCase(); const cacheKey = `${pieceType}_${normColor}_${isLeader ? '1' : '0'}`; const cached = spriteDataUrlCache.get(cacheKey); if (cached) { return cached; } const canvas = getSpriteCanvas(pieceType, normColor, isLeader); const dataUrl = canvas.toDataURL('image/png'); spriteDataUrlCache.set(cacheKey, dataUrl); return dataUrl; } // Canvas Drawing Helper for BoardCanvas export function drawPlayerPiece( ctx: CanvasRenderingContext2D, player: Player, px: number, py: number, cellSize: number, isSelected: boolean, isCurrentTurn: boolean ): void { const pieceType = getPlayerPieceType(player); const isLeader = player.is_party_leader; const spriteCanvas = getSpriteCanvas(pieceType, player.color, isLeader); // Scaled miniature size: board game miniature looks best at ~1.35x cellSize const spriteSize = Math.max(cellSize * 1.35, 14); const destX = Math.round(px - spriteSize / 2); // Center the pedestal base right on (px, py) const destY = Math.round(py - spriteSize * 0.62); ctx.save(); // Active turn indicator glowing ring around the pedestal if (isCurrentTurn) { ctx.save(); ctx.beginPath(); ctx.ellipse(px, py + spriteSize * 0.18, spriteSize * 0.55, spriteSize * 0.28, 0, 0, Math.PI * 2); ctx.fillStyle = 'rgba(245, 158, 11, 0.28)'; ctx.fill(); ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 1.8; ctx.shadowColor = '#f59e0b'; ctx.shadowBlur = 8; ctx.stroke(); ctx.restore(); } // Selected player dashed ring around the pedestal if (isSelected) { ctx.save(); ctx.beginPath(); ctx.ellipse(px, py + spriteSize * 0.18, spriteSize * 0.62, spriteSize * 0.32, 0, 0, Math.PI * 2); ctx.strokeStyle = '#38bdf8'; ctx.lineWidth = 2; ctx.setLineDash([3, 3]); ctx.shadowColor = '#38bdf8'; ctx.shadowBlur = 6; ctx.stroke(); ctx.restore(); } // Draw the crisp pixelated Knight or Warrior figurine ctx.imageSmoothingEnabled = false; ctx.drawImage(spriteCanvas, destX, destY, spriteSize, spriteSize); // Crown symbol above party leader if (isLeader) { ctx.save(); ctx.fillStyle = '#fbbf24'; ctx.font = `${Math.max(spriteSize * 0.45, 10)}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('👑', px, destY - 4); ctx.restore(); } // Player Name and Strength Badge (Title Bar) ctx.save(); const isHighlighted = isCurrentTurn || isSelected; ctx.globalAlpha = isHighlighted ? 1.0 : 0.5; ctx.font = `bold ${Math.max(10, Math.min(12, cellSize * 0.42))}px Inter, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const roleIcon = isLeader ? '👑' : pieceType === 'knight' ? '⚔️' : '🪓'; const hpBadge = player.health !== undefined ? ` ❤️${player.health}` : ''; const text = `${roleIcon} ${player.name} [⚡${player.strength.toFixed(1)}${hpBadge}]`; const textMetrics = ctx.measureText(text); const bgWidth = textMetrics.width + 12; const bgHeight = 16; const labelY = isLeader ? destY - 14 : destY - 8; ctx.fillStyle = 'rgba(15, 23, 42, 0.92)'; ctx.strokeStyle = isLeader ? '#fbbf24' : isCurrentTurn ? '#f59e0b' : player.color; ctx.lineWidth = 1; ctx.beginPath(); ctx.roundRect(px - bgWidth / 2, labelY - 12, bgWidth, bgHeight, 4); ctx.fill(); ctx.stroke(); ctx.fillStyle = isLeader ? '#fef08a' : isCurrentTurn ? '#fbbf24' : '#f8fafc'; ctx.fillText(text, px, labelY - 4); ctx.restore(); ctx.restore(); } // Canvas Drawing Helper for the Grand Wizard NPC export function drawWizardPiece( ctx: CanvasRenderingContext2D, wizard: { x: number; y: number; name: string; strength: number; color?: string }, px: number, py: number, cellSize: number ): void { const color = wizard.color || '#A855F7'; const spriteCanvas = getSpriteCanvas('wizard', color, false); const spriteSize = Math.max(cellSize * 1.45, 16); const destX = Math.round(px - spriteSize / 2); const destY = Math.round(py - spriteSize * 0.62); ctx.save(); // Glowing mystic arcane aura ctx.save(); ctx.beginPath(); ctx.ellipse(px, py + spriteSize * 0.18, spriteSize * 0.65, spriteSize * 0.32, 0, 0, Math.PI * 2); ctx.fillStyle = 'rgba(168, 85, 247, 0.25)'; ctx.fill(); ctx.strokeStyle = '#c084fc'; ctx.lineWidth = 2; ctx.shadowColor = '#c084fc'; ctx.shadowBlur = 12; ctx.stroke(); ctx.restore(); // Draw the pixelated Wizard ctx.imageSmoothingEnabled = false; ctx.drawImage(spriteCanvas, destX, destY, spriteSize, spriteSize); // Floating magic sparkle above wizard hat ctx.save(); ctx.fillStyle = '#fef08a'; ctx.font = `${Math.max(spriteSize * 0.4, 10)}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('✨', px, destY - 6); ctx.restore(); // Wizard Name & Strength Badge ctx.save(); ctx.font = `bold ${Math.max(10, Math.min(12, cellSize * 0.42))}px Inter, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const text = `🧙 ${wizard.name} [⚡${wizard.strength.toFixed(1)}]`; const textMetrics = ctx.measureText(text); const bgWidth = textMetrics.width + 12; const bgHeight = 16; const labelY = destY - 16; ctx.fillStyle = 'rgba(15, 23, 42, 0.94)'; ctx.strokeStyle = '#c084fc'; ctx.lineWidth = 1.2; ctx.shadowColor = '#c084fc'; ctx.shadowBlur = 8; ctx.beginPath(); ctx.roundRect(px - bgWidth / 2, labelY - 12, bgWidth, bgHeight, 4); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#e9d5ff'; ctx.fillText(text, px, labelY - 4); ctx.restore(); ctx.restore(); } // React Component for displaying pixel avatar in UI interface PixelAvatarProps { pieceType: PieceType; color: string; isLeader?: boolean; size?: number; className?: string; title?: string; } export const PixelAvatar: React.FC = ({ pieceType, color, isLeader = false, size = 32, className = '', title, }) => { const dataUrl = getSpriteDataUrl(pieceType, color, isLeader); return ( {`${isLeader ); };