used new icons for mtg symbols
Some checks failed
Build and Deploy / build (push) Failing after 10m12s

This commit is contained in:
2025-12-22 18:02:00 +01:00
parent 5b601efcb6
commit 5dbfd006c2
7 changed files with 181 additions and 20 deletions

View File

@@ -0,0 +1,54 @@
import React from 'react';
type ManaSymbol =
| 'w' // White
| 'u' // Blue
| 'b' // Black
| 'r' // Red
| 'g' // Green
| 'c' // Colorless
| 'x' | 'y' | 'z' // Variables
| 't' | 'tap' // Tap
| 'q' | 'untap' // Untap
| 'e' | 'energy' // Energy
| 'p' // Phyrexian generic? (check font)
| 'vp' // Velcro/Planechase?
| 's' // Snow
| '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' // Numbers
| '10' | '11' | '12' | '13' | '14' | '15' | '16' | '17' | '18' | '19' | '20' // Higher numbers usually specialized, check support
| 'infinity'
| string; // Allow others
interface ManaIconProps {
symbol: ManaSymbol;
size?: 'sm' | 'md' | 'lg' | 'xl' | '2x' | '3x' | '4x' | '5x'; // 'ms-2x' etc from the font or custom sizing
className?: string;
shadow?: boolean; // 'ms-cost' adds a shadow usually
fixedWidth?: boolean; // 'ms-fw'
}
export const ManaIcon: React.FC<ManaIconProps> = ({
symbol,
size,
className = '',
shadow = false,
fixedWidth = false,
}) => {
// Normalize symbol to lowercase
const sym = symbol.toLowerCase();
// Construct class names
// ms is the base class
const classes = [
'ms',
`ms-${sym}`,
size ? `ms-${size}` : '',
shadow ? 'ms-cost' : '', // 'ms-cost' is often used formana costs to give them a circle/shadow look.
fixedWidth ? 'ms-fw' : '',
className,
]
.filter(Boolean)
.join(' ');
return <i className={classes} title={`Mana symbol: ${symbol}`} aria-hidden="true" />;
};

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { App } from './App'; import { App } from './App';
import './styles/main.css'; import './styles/main.css';
import 'mana-font/css/mana.min.css';
import { registerSW } from 'virtual:pwa-register'; import { registerSW } from 'virtual:pwa-register';
// Register Service Worker // Register Service Worker

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useConfirm } from '../../components/ConfirmDialog'; import { useConfirm } from '../../components/ConfirmDialog';
import { ChevronLeft, Eye, RotateCcw } from 'lucide-react'; import { ChevronLeft, Eye, RotateCcw } from 'lucide-react';
import { ManaIcon } from '../../components/ManaIcon';
import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core'; import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { GameState, CardInstance } from '../../types/game'; import { GameState, CardInstance } from '../../types/game';
@@ -15,6 +16,7 @@ import { GestureManager } from './GestureManager';
import { MulliganView } from './MulliganView'; import { MulliganView } from './MulliganView';
import { RadialMenu, RadialOption } from './RadialMenu'; import { RadialMenu, RadialOption } from './RadialMenu';
import { InspectorOverlay } from './InspectorOverlay'; import { InspectorOverlay } from './InspectorOverlay';
import { formatOracleText } from '../../utils/textUtils';
// --- DnD Helpers --- // --- DnD Helpers ---
const DraggableCardWrapper = ({ children, card, disabled }: { children: React.ReactNode, card: CardInstance, disabled?: boolean }) => { const DraggableCardWrapper = ({ children, card, disabled }: { children: React.ReactNode, card: CardInstance, disabled?: boolean }) => {
@@ -224,12 +226,12 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
if (card) { if (card) {
setRadialPosition({ x: payload.x || window.innerWidth / 2, y: payload.y || window.innerHeight / 2 }); setRadialPosition({ x: payload.x || window.innerWidth / 2, y: payload.y || window.innerHeight / 2 });
setRadialOptions([ setRadialOptions([
{ id: 'W', label: 'White', color: '#f0f2eb', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'W' } }) }, { id: 'W', label: 'White', icon: <ManaIcon symbol="w" size="2x" shadow />, color: '#f0f2eb', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'W' } }) },
{ id: 'U', label: 'Blue', color: '#aae0fa', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'U' } }) }, { id: 'U', label: 'Blue', icon: <ManaIcon symbol="u" size="2x" shadow />, color: '#aae0fa', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'U' } }) },
{ id: 'B', label: 'Black', color: '#cbc2bf', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'B' } }) }, { id: 'B', label: 'Black', icon: <ManaIcon symbol="b" size="2x" shadow />, color: '#cbc2bf', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'B' } }) },
{ id: 'R', label: 'Red', color: '#f9aa8f', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'R' } }) }, { id: 'R', label: 'Red', icon: <ManaIcon symbol="r" size="2x" shadow />, color: '#f9aa8f', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'R' } }) },
{ id: 'G', label: 'Green', color: '#9bd3ae', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'G' } }) }, { id: 'G', label: 'Green', icon: <ManaIcon symbol="g" size="2x" shadow />, color: '#9bd3ae', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'G' } }) },
{ id: 'C', label: 'Colorless', color: '#ccc2c0', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'C' } }) }, { id: 'C', label: 'Colorless', icon: <ManaIcon symbol="c" size="2x" shadow />, color: '#ccc2c0', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'C' } }) },
]); ]);
} }
return; return;
@@ -543,7 +545,12 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
<h3 className="text-lg font-bold text-slate-200 leading-tight">{hoveredCard.name}</h3> <h3 className="text-lg font-bold text-slate-200 leading-tight">{hoveredCard.name}</h3>
{hoveredCard.manaCost && ( {hoveredCard.manaCost && (
<p className="text-sm text-slate-400 mt-1 font-mono tracking-widest">{hoveredCard.manaCost}</p> <div className="mt-1 flex items-center text-slate-400">
{hoveredCard.manaCost.match(/\{([^}]+)\}/g)?.map((s, i) => {
const sym = s.replace(/[{}]/g, '').toLowerCase().replace('/', '');
return <ManaIcon key={i} symbol={sym} shadow className="text-base mr-0.5" />;
}) || <span className="font-mono">{hoveredCard.manaCost}</span>}
</div>
)} )}
{hoveredCard.typeLine && ( {hoveredCard.typeLine && (
@@ -552,9 +559,10 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
</div> </div>
)} )}
{hoveredCard.oracleText && ( {hoveredCard.oracleText && (
<div className="text-sm text-slate-300 text-left bg-slate-900/50 p-3 rounded-lg border border-slate-800 whitespace-pre-wrap leading-relaxed shadow-inner"> <div className="text-sm text-slate-300 text-left bg-slate-900/50 p-3 rounded-lg border border-slate-800 leading-relaxed shadow-inner">
{hoveredCard.oracleText} {formatOracleText(hoveredCard.oracleText)}
</div> </div>
)} )}
</div> </div>
@@ -937,21 +945,16 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
</div> </div>
</div> </div>
{/* Mana Pool Display */}
{/* Mana Pool Display */} {/* Mana Pool Display */}
<div className="w-full bg-slate-800/50 rounded-lg p-2 grid grid-cols-3 gap-x-1 gap-y-1 border border-white/5"> <div className="w-full bg-slate-800/50 rounded-lg p-2 grid grid-cols-3 gap-x-1 gap-y-1 border border-white/5">
{['W', 'U', 'B', 'R', 'G', 'C'].map(color => { {['W', 'U', 'B', 'R', 'G', 'C'].map(color => {
const count = myPlayer?.manaPool?.[color] || 0; const count = myPlayer?.manaPool?.[color] || 0;
const icons: Record<string, string> = { // Use ManaIcon instead of emojis
W: '☀️', U: '💧', B: '💀', R: '🔥', G: '🌳', C: '💎'
};
const colors: Record<string, string> = {
W: 'text-yellow-100', U: 'text-blue-300', B: 'text-slate-400', R: 'text-red-400', G: 'text-green-400', C: 'text-slate-300'
};
return ( return (
<div key={color} className="flex flex-col items-center"> <div key={color} className="flex flex-col items-center">
<div className={`text-xs ${colors[color]} font-bold flex items-center gap-1`}> <div className={`text-xs font-bold flex items-center gap-1`}>
{icons[color]} <ManaIcon symbol={color.toLowerCase()} size="lg" shadow />
</div> </div>
<div className="flex items-center gap-1 mt-1"> <div className="flex items-center gap-1 mt-1">

View File

@@ -1,6 +1,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { GameState, Phase, Step } from '../../types/game'; import { GameState, Phase, Step } from '../../types/game';
import { Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Play, RotateCcw, Clock, Files, Crosshair, Skull, Flag, Moon, Trash2 } from 'lucide-react'; import { ManaIcon } from '../../components/ManaIcon';
import { Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Play, Clock, Files, Crosshair, Skull, Flag, Moon, Trash2 } from 'lucide-react';
interface PhaseStripProps { interface PhaseStripProps {
gameState: GameState; gameState: GameState;
@@ -115,7 +116,7 @@ export const PhaseStrip: React.FC<PhaseStripProps> = ({
} }
const stepsList: VisualStep[] = useMemo(() => [ const stepsList: VisualStep[] = useMemo(() => [
{ id: 'untap', label: 'Untap', icon: RotateCcw, phase: 'beginning', step: 'untap' }, { id: 'untap', label: 'Untap', icon: (props: any) => <ManaIcon symbol="untap" className="text-current" {...props} />, phase: 'beginning', step: 'untap' },
{ id: 'upkeep', label: 'Upkeep', icon: Clock, phase: 'beginning', step: 'upkeep' }, { id: 'upkeep', label: 'Upkeep', icon: Clock, phase: 'beginning', step: 'upkeep' },
{ id: 'draw', label: 'Draw', icon: Files, phase: 'beginning', step: 'draw' }, { id: 'draw', label: 'Draw', icon: Files, phase: 'beginning', step: 'draw' },
{ id: 'main1', label: 'Main 1', icon: Zap, phase: 'main1', step: 'main' }, { id: 'main1', label: 'Main 1', icon: Zap, phase: 'main1', step: 'main' },

View File

@@ -0,0 +1,94 @@
import React from 'react';
import { ManaIcon } from '../components/ManaIcon';
/**
* Helper to parse a text segment and replace {X} symbols with icons.
*/
const parseSymbols = (text: string): React.ReactNode => {
if (!text) return null;
const parts = text.split(/(\{.*?\})/g);
return (
<>
{parts.map((part, index) => {
if (part.startsWith('{') && part.endsWith('}')) {
let content = part.slice(1, -1).toLowerCase();
content = content.replace('/', '');
// Manual mapping for special symbols
const symbolMap: Record<string, string> = {
't': 'tap',
'q': 'untap',
};
if (symbolMap[content]) {
content = symbolMap[content];
}
return (
<ManaIcon
key={index}
symbol={content}
className="text-[0.9em] text-slate-900 mx-[1px] align-baseline inline-block"
shadow
/>
);
}
return <span key={index}>{part}</span>;
})}
</>
);
};
/**
* Parses a string containing Magic: The Gathering symbols and lists.
* Replaces symbols with ManaIcon components and bulleted lists with HTML structure.
*/
export const formatOracleText = (text: string | null | undefined): React.ReactNode => {
if (!text) return null;
// Split by specific bullet character or newlines first
// Some cards use actual newlines for abilities, some use bullets for modes.
// We want to handle "•" as a list item start.
// Strategy:
// 1. Split by newline to respect existing paragraph breaks.
// 2. Inside each paragraph, check for bullets.
const lines = text.split('\n');
return (
<div className="flex flex-col gap-1">
{lines.map((line, lineIdx) => {
if (!line.trim()) return null;
// Check for bullets
if (line.includes('•')) {
const segments = line.split('•');
return (
<div key={lineIdx} className="flex flex-col gap-0.5">
{segments.map((seg, segIdx) => {
const content = seg.trim();
if (!content) return null;
// If it's the very first segment and the line didn't start with bullet, it's intro text.
// If the line started with "•", segments[0] is empty (handled above).
const isListItem = segIdx > 0 || line.trim().startsWith('•');
return (
<div key={segIdx} className={`flex gap-1 ${isListItem ? 'ml-2 pl-2 border-l-2 border-white/10' : ''}`}>
{isListItem && <span className="text-emerald-400 font-bold"></span>}
<span className={isListItem ? "text-slate-200" : ""}>{parseSymbols(content)}</span>
</div>
);
})}
</div>
);
}
return <div key={lineIdx}>{parseSymbols(line)}</div>;
})}
</div>
);
};

7
src/package-lock.json generated
View File

@@ -16,6 +16,7 @@
"express": "^4.21.2", "express": "^4.21.2",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"mana-font": "^1.18.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
@@ -5417,6 +5418,12 @@
"sourcemap-codec": "^1.4.8" "sourcemap-codec": "^1.4.8"
} }
}, },
"node_modules/mana-font": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/mana-font/-/mana-font-1.18.0.tgz",
"integrity": "sha512-PKCR5z/eeOBbox0Lu5b6A0QUWVUUa3A3LWXb9sw4N5ThIWXxRbCagXPSL4k6Cnh2Fre6Y4+2Xl819OrY7m1cUA==",
"license": "MIT"
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",

View File

@@ -19,6 +19,7 @@
"express": "^4.21.2", "express": "^4.21.2",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"mana-font": "^1.18.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",