used new icons for mtg symbols
Some checks failed
Build and Deploy / build (push) Failing after 10m12s
Some checks failed
Build and Deploy / build (push) Failing after 10m12s
This commit is contained in:
54
src/client/src/components/ManaIcon.tsx
Normal file
54
src/client/src/components/ManaIcon.tsx
Normal 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" />;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
import './styles/main.css';
|
||||
import 'mana-font/css/mana.min.css';
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
|
||||
// Register Service Worker
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useConfirm } from '../../components/ConfirmDialog';
|
||||
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 { CSS } from '@dnd-kit/utilities';
|
||||
import { GameState, CardInstance } from '../../types/game';
|
||||
@@ -15,6 +16,7 @@ import { GestureManager } from './GestureManager';
|
||||
import { MulliganView } from './MulliganView';
|
||||
import { RadialMenu, RadialOption } from './RadialMenu';
|
||||
import { InspectorOverlay } from './InspectorOverlay';
|
||||
import { formatOracleText } from '../../utils/textUtils';
|
||||
|
||||
// --- DnD Helpers ---
|
||||
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) {
|
||||
setRadialPosition({ x: payload.x || window.innerWidth / 2, y: payload.y || window.innerHeight / 2 });
|
||||
setRadialOptions([
|
||||
{ id: 'W', label: 'White', 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: 'B', label: 'Black', 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: 'G', label: 'Green', 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: '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', 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', 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', 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', 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', icon: <ManaIcon symbol="c" size="2x" shadow />, color: '#ccc2c0', onSelect: () => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', color: 'C' } }) },
|
||||
]);
|
||||
}
|
||||
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>
|
||||
|
||||
{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 && (
|
||||
@@ -552,9 +559,10 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{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">
|
||||
{hoveredCard.oracleText}
|
||||
<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">
|
||||
{formatOracleText(hoveredCard.oracleText)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -937,21 +945,16 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{['W', 'U', 'B', 'R', 'G', 'C'].map(color => {
|
||||
const count = myPlayer?.manaPool?.[color] || 0;
|
||||
const icons: Record<string, string> = {
|
||||
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'
|
||||
};
|
||||
|
||||
// Use ManaIcon instead of emojis
|
||||
return (
|
||||
<div key={color} className="flex flex-col items-center">
|
||||
<div className={`text-xs ${colors[color]} font-bold flex items-center gap-1`}>
|
||||
{icons[color]}
|
||||
<div className={`text-xs font-bold flex items-center gap-1`}>
|
||||
<ManaIcon symbol={color.toLowerCase()} size="lg" shadow />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
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 {
|
||||
gameState: GameState;
|
||||
@@ -115,7 +116,7 @@ export const PhaseStrip: React.FC<PhaseStripProps> = ({
|
||||
}
|
||||
|
||||
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: 'draw', label: 'Draw', icon: Files, phase: 'beginning', step: 'draw' },
|
||||
{ id: 'main1', label: 'Main 1', icon: Zap, phase: 'main1', step: 'main' },
|
||||
|
||||
94
src/client/src/utils/textUtils.tsx
Normal file
94
src/client/src/utils/textUtils.tsx
Normal 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
7
src/package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"express": "^4.21.2",
|
||||
"ioredis": "^5.8.2",
|
||||
"lucide-react": "^0.475.0",
|
||||
"mana-font": "^1.18.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
@@ -5417,6 +5418,12 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"express": "^4.21.2",
|
||||
"ioredis": "^5.8.2",
|
||||
"lucide-react": "^0.475.0",
|
||||
"mana-font": "^1.18.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
|
||||
Reference in New Issue
Block a user