import { useState, useEffect, useRef } from "react"; // ============================================================ // ELEMENT SYSTEM // ============================================================ const ELEMENT_CYCLE = { fire: { beats: "ice", weakTo: "water", icon: "šŸ”„", color: "#e74c3c" }, ice: { beats: "water", weakTo: "fire", icon: "ā„ļø", color: "#3498db" }, water: { beats: "lightning", weakTo: "ice", icon: "šŸ’§", color: "#1abc9c" }, lightning: { beats: "earth", weakTo: "water", icon: "⚔", color: "#f39c12" }, earth: { beats: "wind", weakTo: "lightning", icon: "šŸŒ", color: "#8b4513" }, wind: { beats: "fire", weakTo: "earth", icon: "šŸŒŖļø", color: "#7f8c8d" }, light: { beats: "dark", weakTo: "dark", icon: "✨", color: "#f1c40f" }, dark: { beats: "light", weakTo: "light", icon: "šŸŒ‘", color: "#8e44ad" } }; // ============================================================ // GENERATE 1000 CARDS // ============================================================ const CREATURE_TEMPLATES = { fire: ["Emberclaw", "Pyroclast", "Ashwing", "Flamebound", "Infernal", "Scorchwyrm", "Cinder", "Blaze", "Magmafist", "Burnheart", "Searing", "Flamebeast", "Lavaborn", "Pyreking", "Cinderhorn", "Charred", "Heatwave", "Molten", "Wildfire", "Firestorm"], ice: ["Frostbite", "Glacius", "Hoarfrost", "Icebound", "Permafrost", "Snowfall", "Crystalix", "Winterclaw", "Frostveil", "Coldsnap", "Icicle", "Blizzard", "Frostguard", "Glacial", "Rimefrost", "Chillborn", "Arcticus", "Snowdrift", "Frostfang", "Iceheart"], water: ["Tidecaller", "Deepwarden", "Coralborn", "Stormwake", "Abyssal", "Wavecrest", "Kelphook", "Maelstrom", "Aquarius", "Seafoam", "Torrent", "Whirlpool", "Oceanborn", "Depthstalker", "Riptide", "Seabeast", "Current", "Mariner", "Saltborn", "Deepsea"], lightning: ["Voltspine", "Stormborn", "Thunderclaw", "Shockwave", "Arclash", "Strikeborn", "Ionflux", "Staticbane", "Voltaic", "Tempest", "Boltborn", "Sparkfang", "Thunderborn", "Surgecoil", "Electrus", "Shockfist", "Stormchaser", "Ionborn", "Chargemaster", "Arcflash"], earth: ["Stoneheart", "Quartzback", "Terrabane", "Cliffborn", "Boulderfist", "Gravelord", "Ironveil", "Cragborn", "Rockfist", "Earthshaker", "Granite", "Gemborn", "Mountainborn", "Cliffjaw", "Stonewall", "Dirtborn", "Quakehoof", "Stoneborn", "Earthling", "Crystalback"], wind: ["Tempest", "Zephyrwing", "Galeforce", "Skydancer", "Cyclone", "Breezeborn", "Whirlwind", "Draftmaw", "Stormrider", "Airborn", "Gustwing", "Typhoon", "Skyborn", "Windrider", "Breezehowl", "Vortex", "Galebeast", "Windborn", "Hurricane", "Squall"], light: ["Radiant", "Dawnbringer", "Luminary", "Solaris", "Holyflame", "Starborn", "Prismatic", "Lightbane", "Celestial", "Sunborn", "Divineray", "Glowborn", "Lux", "Holylight", "Shimmer", "Brightborn", "Aurora", "Daylord", "Holybeam", "Starlight"], dark: ["Shadowmaw", "Voidcaller", "Duskborn", "Nightshade", "Abyssal", "Grimveil", "Eclipseborn", "Netherbane", "Darkborn", "Voidfang", "Nightborn", "Shadowclaw", "Duskfang", "Nightmaw", "Darkflux", "Obsidian", "Midnight", "Gloomborn", "Shadowborn", "Darkveil"] }; const DESCRIPTORS = ["fearsome beast wielding", "ancient creature of pure", "powerful entity commanding", "mystical being born from", "legendary monster channeling", "fierce predator infused with"]; function generateMonsters() { const monsters = []; let id = 1; const elements = Object.keys(ELEMENT_CYCLE); for (let i = 0; i < 750; i++) { const element = elements[i % elements.length]; const templates = CREATURE_TEMPLATES[element]; const name = templates[Math.floor(Math.random() * templates.length)]; const descriptor = DESCRIPTORS[Math.floor(Math.random() * DESCRIPTORS.length)]; const stars = Math.floor(i / 75) + 1; monsters.push({ id: id++, cardNumber: `SH-${String(id).padStart(3, '0')}`, name, type: "monster", stars, atk: 100 + stars * 200 + Math.floor(Math.random() * 300), def: 80 + stars * 150 + Math.floor(Math.random() * 250), hp: 300 + stars * 250 + Math.floor(Math.random() * 400), rarity: stars <= 2 ? "common" : stars <= 4 ? "uncommon" : stars <= 7 ? "rare" : "super_rare", element, desc: `${descriptor} ${element} energy`, effect: Math.random() < 0.25 ? { desc: `On summon: Deal ${stars * 50} damage to opponent`, trigger:"onSummon", type:"directDamage", value: stars * 50 } : null, sacrifices: stars >= 10 ? 2 : stars >= 5 ? 1 : 0 }); } return monsters; } function generateMagicCards() { const spells = [ { name: "Soul Revival", desc: "Powerful resurrection spell", effect: "Special Summon 1 monster from your Graveyard" }, { name: "Heavenstrike", desc: "Divine judgment from above", effect: "Deal 800 damage to target creature" }, { name: "Battle Surge", desc: "Primal strength enhancement", effect: "Target creature gains +600 ATK this turn" }, { name: "Iron Bastion", desc: "Impenetrable defenses", effect: "Target creature gains +700 DEF this turn" }, { name: "Mending Draught", desc: "Healing elixir", effect: "Restore 1000 HP to yourself" }, { name: "Scroll of Fortune", desc: "Ancient knowledge", effect: "Draw 2 cards from your deck" }, { name: "Fireball", desc: "Explosive flame", effect: "Deal 600 damage to target" }, { name: "Lightning Bolt", desc: "Crackling electricity", effect: "Deal 700 damage and stun target" } ]; const magicCards = []; for (let i = 0; i < 200; i++) { const spell = spells[i % spells.length]; magicCards.push({ id: 1000 + i, cardNumber: `SH-M${String(i+1).padStart(3, '0')}`, name: spell.name, type: "magic", rarity: i < 50 ? "common" : i < 120 ? "uncommon" : i < 180 ? "rare" : "super_rare", desc: spell.desc, effectDesc: spell.effect }); } return magicCards; } function generateTrapCards() { const traps = [ { name: "Mirror Force", desc: "Reflective barrier", effect: "When attacked: Destroy all opponent's Attack Position monsters" }, { name: "Trap Hole", desc: "Hidden pitfall", effect: "When opponent summons (1000+ ATK): Destroy that monster" }, { name: "Magic Cylinder", desc: "Redirect damage", effect: "When attacked: Negate attack and deal attacker's ATK as damage" }, { name: "Sakuretsu Armor", desc: "Explosive counter", effect: "When attacked: Destroy the attacking monster" } ]; const trapCards = []; for (let i = 0; i < 50; i++) { const trap = traps[i % traps.length]; trapCards.push({ id: 2000 + i, cardNumber: `SH-T${String(i+1).padStart(3, '0')}`, name: trap.name, type: "trap", rarity: i < 20 ? "common" : i < 35 ? "uncommon" : i < 45 ? "rare" : "super_rare", desc: trap.desc, effectDesc: trap.effect }); } return trapCards; } const MONSTERS = generateMonsters(); const MAGIC_CARDS = generateMagicCards(); const TRAP_CARDS = generateTrapCards(); const ALL_CARDS = [...MONSTERS, ...MAGIC_CARDS, ...TRAP_CARDS]; // ============================================================ // REFRESHED STORY EVENTS // ============================================================ const STORY_EVENTS = [ { id:"e1", text:"A mysterious merchant spreads his wares before you. 'These cards hold power beyond measure,' he whispers. 'But power always comes with a price...'", options:["Examine his cards →","Ask about his past →","Walk away suspicious →","Negotiate prices →"] }, { id:"e2", text:"Thunder rumbles overhead as a cloaked figure challenges you. 'Your reputation precedes you, duelist. Let's see if you're worthy of it.'", options:["Accept the duel →","Question their motives →","Propose a wager →","Decline politely →"] }, { id:"e3", text:"You discover an ancient shrine dedicated to forgotten duel monsters. Strange energy emanates from within, calling to you.", options:["Enter the shrine →","Study the inscriptions →","Leave offerings →","Turn back →"] }, { id:"e4", text:"A grand tournament is announced! The Shadow Realm Championship promises glory, gold, and a legendary prize card to the victor.", options:["Register for tournament →","Scout other duelists →","Train your deck →","Gather information →"], triggerTournament: true }, { id:"e5", text:"A wounded duelist stumbles toward you. 'Please... take my deck... don't let them fall into the wrong hands...' They collapse before you can ask more.", options:["Help them immediately →","Examine the deck →","Search for pursuers →","Call for healers →"] }, { id:"e6", text:"You overhear rumors of a secret underground dueling ring where rare cards exchange hands. The entrance is hidden, but you know someone who knows the way.", options:["Seek the entrance →","Ask your contact →","Investigate cautiously →","Report to authorities →"] }, ]; // ============================================================ // UTILITIES // ============================================================ const RARITY_COLORS = { common:"#888", uncommon:"#4caf50", rare:"#2196f3", super_rare:"#ff9800", legendary:"#ff00ff" }; const SAVE_KEY = "duel_masters_v1"; function saveGame(s) { try { localStorage.setItem(SAVE_KEY, JSON.stringify(s)); } catch(e){} } function loadGame() { try { const s = localStorage.getItem(SAVE_KEY); return s ? JSON.parse(s) : null; } catch(e){ return null; } } function shuffle(arr) { const a=[...arr]; for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];} return a; } function deepClone(o) { return JSON.parse(JSON.stringify(o)); } function getCardKey(c) { return `${c.id}_${c.cardNumber}`; } function getCardQuantities(collection) { const counts = {}; collection.forEach(card => { const key = getCardKey(card); counts[key] = (counts[key] || 0) + 1; }); return counts; } function countCardInDeck(deck, card) { const key = getCardKey(card); return deck.filter(c => getCardKey(c) === key).length; } function buildStarterDeck() { return [ ...shuffle(MONSTERS.filter(m => m.stars <= 2 && m.rarity === "common")).slice(0,15), ...shuffle(MAGIC_CARDS.filter(m => m.rarity === "common")).slice(0,6), ...shuffle(TRAP_CARDS.filter(t => t.rarity === "common")).slice(0,4) ]; } // ============================================================ // CARD DISPLAY WITH HOVER // ============================================================ function CardDisplay({ card, onClick, selected, small, quantity, inDeckCount, notOwned, showHoverPreview, isNew }) { const [hovered, setHovered] = useState(false); if (!card) return null; const elInfo = card.element ? ELEMENT_CYCLE[card.element] : null; const w = small ? 80 : 120, h = small ? 112 : 168; const typeColor = card.type==="monster" ? (elInfo?.color||"#666") : card.type==="magic" ? "#4a90e2" : "#c0392b"; return (
showHoverPreview && setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ position: "relative", display: "inline-block" }} >
{card.cardNumber} {card.type==="monster" && {"ā˜…".repeat(Math.min(card.stars,5))}}
{card.name}
{card.type==="monster"?"šŸŒ™":card.type==="magic"?"✨":"⚔"}
{card.type==="monster" && (
{[["ATK",card.atk],["DEF",card.def],["HP",card.hp]].map(([lbl,val])=>(
{lbl}
{val}
))}
)} {!small && (
{card.type==="monster" ? ( <> {elInfo && {elInfo.icon}} {card.effect ? {card.effect.desc} : {card.desc}} ) : ( {card.effectDesc || card.desc} )}
)}
{card.rarity?.replace("_"," ")} {elInfo && {card.element}}
{quantity > 1 &&
Ɨ{quantity}
} {inDeckCount > 0 &&
šŸ“‹ {inDeckCount}
} {isNew &&
*NEW*
} {notOwned &&
šŸ”’
}
{hovered && showHoverPreview && !selected && (
)}
); } function EnlargedCard({ card }) { const elInfo = card.element ? ELEMENT_CYCLE[card.element] : null; const typeColor = card.type==="monster" ? (elInfo?.color||"#666") : card.type==="magic" ? "#4a90e2" : "#c0392b"; return (
{card.cardNumber} {card.type === "monster" && {"ā˜…".repeat(Math.min(card.stars, 5))}}
{card.name}
{card.type.toUpperCase()}
{card.type === "monster" ? "šŸŒ™" : card.type === "magic" ? "✨" : "⚔"}
{card.type === "monster" && (
{[["ATK", card.atk], ["DEF", card.def], ["HP", card.hp]].map(([lbl, val]) => (
{lbl}
{val}
))}
)}
{card.effect ? ( <>
⚔ CARD EFFECT:
{card.effect.desc}
{elInfo && {elInfo.icon}} {card.desc}
) : card.effectDesc && card.type !== "monster" ? ( <>
⚔ CARD EFFECT:
{card.effectDesc}
{card.desc}
) : (
{elInfo && {elInfo.icon}} {card.desc}
)}
{card.rarity?.replace("_", " ")} {elInfo && {card.element}}
); } // ============================================================ // PINNED CARD PREVIEW (CLICK TO PIN) // ============================================================ function PinnedCardPreview({ card, onAddToDeck, onClose }) { if (!card) return null; return (
e.stopPropagation()} style={{ width: 300 }}>
); } // ============================================================ // STORY SCREEN // ============================================================ function StoryScreen({ playerName, playerGold, onStartTournament, tournamentActive, inBattle, storyState, setStoryState, onStartBattle }) { const [inputText, setInputText] = useState(""); const [loading, setLoading] = useState(false); const logRef = useRef(null); const storyLog = storyState.log; const currentEvent = storyState.currentEvent; const showTournamentButton = storyState.showTournamentButton; const setStoryLog = (updater) => { setStoryState(prev => ({...prev, log: typeof updater === 'function' ? updater(prev.log) : updater})); }; const setCurrentEvent = (event) => { setStoryState(prev => ({...prev, currentEvent: event})); }; const setShowTournamentButton = (val) => { setStoryState(prev => ({...prev, showTournamentButton: val})); }; useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [storyLog]); function addLog(role, text) { setStoryLog(l => [...l, { role, text, id: Date.now()+Math.random() }]); } async function handleOption(option) { addLog("player", option); // Check if accepting a duel - look at recent narrator messages const recentNarrator = storyLog.filter(l => l.role === "narrator").slice(-2); const hasDuelMention = recentNarrator.some(msg => msg.text.toLowerCase().includes("duel") || msg.text.toLowerCase().includes("challenge") || msg.text.toLowerCase().includes("battle") ); const acceptingDuel = option.toLowerCase().includes("accept") || option.toLowerCase().includes("bring it") || option.toLowerCase().includes("let") || option.toLowerCase().includes("begin"); if (hasDuelMention && acceptingDuel) { addLog("narrator", "The challenger grins and draws their deck. 'Let's see what you've got!'"); // Show duel popup after a brief delay setTimeout(() => { const confirmed = window.confirm("āš”ļø BEGIN DUEL?\n\nšŸŽ“ Your opponent is ready!\nšŸ’° Winner takes 50 gold\n\nClick OK to enter the Battle Arena."); if (confirmed) { onStartBattle({ type: "friendly", wager: 50 }); } else { addLog("narrator", "You hesitate... the moment passes. Perhaps another time."); } }, 800); return; // Stop here, don't continue story } setLoading(true); if (currentEvent.triggerTournament && option.toLowerCase().includes("register")) { setShowTournamentButton(true); addLog("narrator", "Excellent! The tournament registration is complete. When you're ready, the tournament will begin. Prepare your deck and steel your resolve."); setLoading(false); return; } try { const context = storyLog.slice(-4).map(l => `${l.role==="player"?"Player":"Narrator"}: ${l.text}`).join("\n"); const response = await fetch("https://api.anthropic.com/v1/messages", { method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify({ model:"claude-sonnet-4-20250514", max_tokens:200, messages:[{ role:"user", content:`You are narrator of dark fantasy card game DUEL MASTERS. Keep response to 2 sentences max, atmospheric. Recent:\n${context}\n\nPlayer: ${option}\n\nRespond as narrator:` }] }) }); const data = await response.json(); const text = data.content?.find(b=>b.type==="text")?.text || "The shadows deepen around you."; addLog("narrator", text); const nextEvent = STORY_EVENTS[Math.floor(Math.random()*STORY_EVENTS.length)]; setCurrentEvent(nextEvent); setTimeout(()=>addLog("narrator", nextEvent.text), 600); } catch(e) { addLog("narrator", "The shadows stir... your choice echoes through the Realm."); const nextEvent = STORY_EVENTS[Math.floor(Math.random()*STORY_EVENTS.length)]; setCurrentEvent(nextEvent); } setLoading(false); } function handleFreeInput() { if (!inputText.trim() || inBattle) return; const text = inputText.trim(); setInputText(""); handleOption(text); } return (

šŸ“– Story Mode

{storyLog.map((entry,i) => (
{entry.role==="player"?playerName:"Narrator"} {entry.text}
))} {loading &&
...
} {inBattle && (
āš”ļø BATTLE IN PROGRESS
Story input disabled until battle ends
)}
{showTournamentButton && !tournamentActive && (
)} {/* Show BEGIN DUEL button if recent narrator mentioned duel */} {!inBattle && storyLog.filter(l => l.role === "narrator").slice(-2).some(msg => msg.text.toLowerCase().includes("duel") || msg.text.toLowerCase().includes("challenge") || msg.text.toLowerCase().includes("grin") ) && (
)} {!loading && !inBattle && currentEvent && (
{currentEvent.options.map((opt,i) => ( ))}
)}
setInputText(e.target.value)} onKeyDown={e=>e.key==="Enter"&&handleFreeInput()} placeholder={inBattle ? "Battle in progress..." : "Type your own action..."} disabled={inBattle} style={{ flex:1, background:"rgba(255,255,255,0.05)", border:"2px solid rgba(255,255,255,0.15)", borderRadius:10, padding:"10px 14px", color:inBattle?"#555":"#fff", fontSize:13, outline:"none", cursor: inBattle ? "not-allowed" : "text" }} />
); } // ============================================================ // DECK BUILDER WITH PINNED PREVIEW // ============================================================ function DeckScreen({ playerDeck, playerCollection, onSave }) { const [deck, setDeck] = useState(deepClone(playerDeck)); const [filter, setFilter] = useState("all"); const [search, setSearch] = useState(""); const [pinnedCard, setPinnedCard] = useState(null); const collectionQty = getCardQuantities(playerCollection); const uniqueCards = []; const seen = new Set(); playerCollection.forEach(card => { const key = getCardKey(card); if (!seen.has(key)) { seen.add(key); uniqueCards.push(card); } }); const filtered = uniqueCards.filter(c => { const matchType = filter === "all" || c.type === filter; const matchSearch = !search || c.name.toLowerCase().includes(search.toLowerCase()); return matchType && matchSearch; }); function addToDeck(card) { const key = getCardKey(card); const ownedQty = collectionQty[key] || 0; const inDeckQty = countCardInDeck(deck, card); if (deck.length >= 30) { setPinnedCard(null); setTimeout(() => alert("āŒ Deck is Full!\n\nMaximum 30 cards allowed in your deck."), 100); return; } if (ownedQty === 0) { setPinnedCard(null); setTimeout(() => alert(`āŒ You Don't Own This Card!\n\n"${card.name}" is not in your collection.`), 100); return; } // Check 2-copy limit FIRST if (inDeckQty >= 2) { setPinnedCard(null); setTimeout(() => alert(`āŒ Deck Limit Reached!\n\nYou can only have 2 copies of "${card.name}" in your deck.\n\nCurrent in deck: ${inDeckQty}/2`), 100); return; } // Then check if you have more copies to add if (inDeckQty >= ownedQty) { setPinnedCard(null); setTimeout(() => alert(`āŒ Not Enough Copies!\n\nYou only own ${ownedQty} ${ownedQty === 1 ? 'copy' : 'copies'} of "${card.name}".\n\nAll ${ownedQty} ${ownedQty === 1 ? 'is' : 'are'} already in your deck.`), 100); return; } setDeck(d => [...d, card]); setPinnedCard(null); } return (

Collection ({playerCollection.length} cards)

{["all","monster","magic","trap"].map(f => ( ))} setSearch(e.target.value)} placeholder="Search..." style={{ background:"rgba(255,255,255,0.05)", border:"1px solid #444", borderRadius:6, padding:"4px 10px", color:"#fff", fontSize:11, outline:"none", flex:1, minWidth:80 }} />
{filtered.map((card,i) => { const key = getCardKey(card); const owned = collectionQty[key] || 0; const inDeck = countCardInDeck(deck, card); const isSelected = pinnedCard && getCardKey(pinnedCard) === key; return (
setPinnedCard(card)} style={{ position: "relative" }}> {owned > 1 &&
Ɨ{owned}
}
); })}

Deck ({deck.length}/30)

{deck.length === 0 &&

Click cards to add

}
{deck.map((card,i) => (
{card.type==="monster"?"šŸŒ™":card.type==="magic"?"✨":"⚔"}
{card.name}
{card.cardNumber}
{card.type==="monster" && {card.atk}A}
))}
{pinnedCard && ( addToDeck(pinnedCard)} onClose={() => setPinnedCard(null)} /> )}
); } // ============================================================ // COLLECTION SCREEN // ============================================================ function CollectionScreen({ collection }) { const collectionQty = getCardQuantities(collection); const uniqueCards = []; const seen = new Set(); collection.forEach(card => { const key = getCardKey(card); if (!seen.has(key)) { seen.add(key); uniqueCards.push(card); } }); return (

šŸŽ“ My Collection ({collection.length} cards)

{uniqueCards.map((card,i) => { const key = getCardKey(card); const qty = collectionQty[key] || 0; return ; })}
); } // ============================================================ // CODEX SCREEN // ============================================================ function CodexScreen({ playerCollection }) { const [filter, setFilter] = useState("all"); const [search, setSearch] = useState(""); const collectionKeys = new Set(playerCollection.map(c => getCardKey(c))); const filtered = ALL_CARDS.filter(c => { const matchType = filter === "all" || c.type === filter; const matchSearch = !search || c.name.toLowerCase().includes(search.toLowerCase()); return matchType && matchSearch; }); const ownedCount = ALL_CARDS.filter(c => collectionKeys.has(getCardKey(c))).length; const percent = Math.floor((ownedCount / ALL_CARDS.length) * 100); return (

šŸ“š Card Codex

{ownedCount} / {ALL_CARDS.length}
{percent}% Complete
{["all","monster","magic","trap"].map(f => ( ))}
setSearch(e.target.value)} placeholder="Search..." style={{ background:"rgba(255,255,255,0.05)", border:"1px solid #444", borderRadius:8, padding:"8px 12px", color:"#fff", fontSize:13, outline:"none", width:"100%" }} />
{filtered.length} cards — šŸ”’ = Not collected
{filtered.map((card,i) => ( ))}
); } // ============================================================ // SHOP SCREEN WITH LIVE TIMER // ============================================================ function ShopScreen({ playerGold, playerCollection, playerDeck, onBuy, onSell }) { const [tab, setTab] = useState("packs"); const [packResult, setPackResult] = useState(null); const [shopSingles, setShopSingles] = useState([]); const [refreshTime, setRefreshTime] = useState(Date.now() + 3600000); const [currentTime, setCurrentTime] = useState(Date.now()); const [sellFilter, setSellFilter] = useState("not_equipped"); useEffect(() => { const saved = localStorage.getItem('shop_singles'); const savedTime = localStorage.getItem('shop_refresh_time'); if (saved && savedTime && parseInt(savedTime) > Date.now()) { setShopSingles(JSON.parse(saved)); setRefreshTime(parseInt(savedTime)); } else { refreshShop(); } // Live timer update every second const interval = setInterval(() => { const now = Date.now(); setCurrentTime(now); if (now >= refreshTime) { refreshShop(); } }, 1000); return () => clearInterval(interval); }, [refreshTime]); function refreshShop() { const newSingles = shuffle(ALL_CARDS).slice(0, 5).map(card => { const price = card.rarity === "common" ? 20 : card.rarity === "uncommon" ? 40 : card.rarity === "rare" ? 80 : 150; return { ...card, price }; }); setShopSingles(newSingles); const newRefresh = Date.now() + 3600000; setRefreshTime(newRefresh); localStorage.setItem('shop_singles', JSON.stringify(newSingles)); localStorage.setItem('shop_refresh_time', newRefresh.toString()); } const timeLeft = Math.max(0, Math.floor((refreshTime - currentTime) / 1000)); const minutes = Math.floor(timeLeft / 60); const seconds = timeLeft % 60; const PACKS = [ { id:"basic", name:"Basic Pack", cost:50, desc:"10 random cards", icon:"šŸ“¦" }, { id:"rare", name:"Rare Pack", cost:120, desc:"6 cards, higher rare chance", icon:"šŸ’Ž" }, ...Object.keys(ELEMENT_CYCLE).map(el => ({ id: el, name: `${el.charAt(0).toUpperCase() + el.slice(1)} Pack`, cost: 80, desc: `5 ${el} element cards`, icon: ELEMENT_CYCLE[el].icon })) ]; function openPack(pack) { if (playerGold < pack.cost) return; let cards = []; if (pack.id === "basic") { cards = shuffle(ALL_CARDS).slice(0, 10); } else if (pack.id === "rare") { const pool = ALL_CARDS.filter(c => c.rarity !== "common" || Math.random() < 0.3); cards = shuffle(pool).slice(0, 6); } else { cards = shuffle(MONSTERS.filter(c => c.element === pack.id)).slice(0, 5); } onBuy(pack.cost, cards); setPackResult(cards); } function buySingle(card) { if (playerGold < card.price) return; onBuy(card.price, [card]); const newSingles = shopSingles.filter(c => c !== card); setShopSingles(newSingles); localStorage.setItem('shop_singles', JSON.stringify(newSingles)); } return (

šŸŖ Card Shop

{["packs","singles","sell"].map(t => ( ))}
{packResult && (
Pack Opened! ({packResult.length} cards)
{packResult.map((c,i) => )}
)} {tab === "packs" && (
{PACKS.map(pack => (
=pack.cost?"#f39c12":"#333"}`, borderRadius:12, padding:16, textAlign:"center" }}>
{pack.icon}
{pack.name}
{pack.desc}
))}
)} {tab === "singles" && (
ā° Refreshes in: {String(minutes).padStart(2,'0')}:{String(seconds).padStart(2,'0')}
{shopSingles.map((card,i) => (
))}
)} {tab === "sell" && (
{["not_equipped","equipped"].map(filter => ( ))}
{(() => { const deckKeys = new Set(playerDeck.map(c => getCardKey(c))); const collectionQty = getCardQuantities(playerCollection); const uniqueCards = []; const seen = new Set(); playerCollection.forEach(card => { const key = getCardKey(card); if (!seen.has(key)) { seen.add(key); const inDeck = countCardInDeck(playerDeck, card); const owned = collectionQty[key] || 0; const canSell = owned > inDeck; const isEquipped = deckKeys.has(key); if ((sellFilter === "equipped" && isEquipped) || (sellFilter === "not_equipped" && canSell)) { uniqueCards.push({card, owned, inDeck, canSell}); } } }); return uniqueCards.map(({card, owned, inDeck, canSell},i) => { const sellPrice = Math.floor((card.rarity === "common" ? 10 : card.rarity === "uncommon" ? 20 : card.rarity === "rare" ? 40 : 75)); return (
); }); })()}
)}
); } // ============================================================ // ELEMENTS SCREEN // ============================================================ function ElementsScreen() { return (

⚔ Element System

Element advantage grants +15% damage. Disadvantage applies -15% damage. All card stats reset to normal after battle ends.

{Object.entries(ELEMENT_CYCLE).map(([el,data]) => (
{data.icon}
{el}
āœ“ Beats {data.beats}
āœ— Weak to {data.weakTo}
))}
); } // ============================================================ // TOURNAMENT SCREEN // ============================================================ // ============================================================ // TOURNAMENT DATA - 20 TOURNAMENTS WITH UNIQUE DUELISTS // ============================================================ const TOURNAMENTS = [ { id: 1, name: "Rookie Arena", difficulty: "Beginner", entryFee: 0, prizeGold: 100, prizeCards: 1, participants: 4 }, { id: 2, name: "Street Duel Circuit", difficulty: "Beginner", entryFee: 50, prizeGold: 200, prizeCards: 2, participants: 4 }, { id: 3, name: "City Championship", difficulty: "Easy", entryFee: 100, prizeGold: 400, prizeCards: 3, participants: 8 }, { id: 4, name: "Regional Qualifiers", difficulty: "Easy", entryFee: 150, prizeGold: 600, prizeCards: 0, participants: 8 }, { id: 5, name: "Academy Tournament", difficulty: "Medium", entryFee: 200, prizeGold: 800, prizeCards: 4, participants: 8 }, { id: 6, name: "Guild Masters Cup", difficulty: "Medium", entryFee: 250, prizeGold: 1000, prizeCards: 5, participants: 8 }, { id: 7, name: "Elemental Showdown", difficulty: "Medium", entryFee: 300, prizeGold: 0, prizeCards: 8, participants: 8 }, { id: 8, name: "Shadow League", difficulty: "Hard", entryFee: 400, prizeGold: 1500, prizeCards: 6, participants: 16 }, { id: 9, name: "Imperial Grand Prix", difficulty: "Hard", entryFee: 500, prizeGold: 2000, prizeCards: 8, participants: 16 }, { id: 10, name: "Twilight Invitational", difficulty: "Hard", entryFee: 600, prizeGold: 2500, prizeCards: 10, participants: 16 }, { id: 11, name: "Masters Championship", difficulty: "Expert", entryFee: 800, prizeGold: 3500, prizeCards: 12, participants: 16 }, { id: 12, name: "Legends Arena", difficulty: "Expert", entryFee: 1000, prizeGold: 5000, prizeCards: 15, participants: 16 }, { id: 13, name: "Platinum Series", difficulty: "Expert", entryFee: 1200, prizeGold: 0, prizeCards: 20, participants: 16 }, { id: 14, name: "Diamond League", difficulty: "Elite", entryFee: 1500, prizeGold: 8000, prizeCards: 18, participants: 32 }, { id: 15, name: "World Championship", difficulty: "Elite", entryFee: 2000, prizeGold: 12000, prizeCards: 25, participants: 32 }, { id: 16, name: "Grand Festival", difficulty: "Elite", entryFee: 2500, prizeGold: 15000, prizeCards: 30, participants: 32 }, { id: 17, name: "Mythic Gauntlet", difficulty: "Legendary", entryFee: 3000, prizeGold: 20000, prizeCards: 40, participants: 32 }, { id: 18, name: "Cosmic Clash", difficulty: "Legendary", entryFee: 4000, prizeGold: 30000, prizeCards: 50, participants: 32 }, { id: 19, name: "Ultimate Showdown", difficulty: "Legendary", entryFee: 5000, prizeGold: 50000, prizeCards: 75, participants: 64 }, { id: 20, name: "Infinity Cup", difficulty: "Legendary", entryFee: 10000, prizeGold: 100000, prizeCards: 100, participants: 64 } ]; const DUELISTS = [ // Beginner tier { name: "Rookie Ryan", deck: "basic_fire", skill: 1, bio: "Just started dueling last week" }, { name: "Timid Tina", deck: "basic_water", skill: 1, bio: "Nervous but determined" }, { name: "Eager Eddie", deck: "basic_earth", skill: 2, bio: "Lots of enthusiasm, little experience" }, { name: "Casual Casey", deck: "basic_wind", skill: 2, bio: "Plays for fun" }, // Easy tier { name: "Street Fighter Sam", deck: "improved_fire", skill: 3, bio: "Veteran of local card shops" }, { name: "Tactical Terry", deck: "improved_lightning", skill: 3, bio: "Studies every matchup" }, { name: "Quick Quinn", deck: "improved_wind", skill: 4, bio: "Fast-paced aggressive duelist" }, { name: "Steady Sarah", deck: "improved_earth", skill: 4, bio: "Defensive playstyle expert" }, // Medium tier { name: "Academy Ace Alex", deck: "advanced_mixed", skill: 5, bio: "Top of their dueling class" }, { name: "Guild Master Morgan", deck: "advanced_dark", skill: 5, bio: "Leader of the Shadow Guild" }, { name: "Elemental Elena", deck: "advanced_light", skill: 6, bio: "Masters all eight elements" }, { name: "Professor Pierce", deck: "advanced_lightning", skill: 6, bio: "Teaches advanced strategies" }, // Hard tier { name: "Shadow Knight Kane", deck: "expert_dark", skill: 7, bio: "Mysterious masked duelist" }, { name: "Imperial Guard Iris", deck: "expert_light", skill: 7, bio: "Protects the royal family" }, { name: "Twilight Sage Silas", deck: "expert_mixed", skill: 8, bio: "Ancient wisdom meets modern tactics" }, { name: "Crimson Duelist Drake", deck: "expert_fire", skill: 8, bio: "Never lost a fire mirror match" }, // Expert tier { name: "Master Zephyr", deck: "master_wind", skill: 9, bio: "Can predict your every move" }, { name: "Legend Luna", deck: "master_light", skill: 9, bio: "Three-time world champion" }, { name: "Platinum Prince Victor", deck: "master_mixed", skill: 10, bio: "Royalty with unmatched skill" }, { name: "Diamond Duchess Diana", deck: "master_ice", skill: 10, bio: "Cold precision in every duel" }, // Elite tier { name: "World Champion Renji", deck: "elite_fire", skill: 11, bio: "Current reigning world champion" }, { name: "Grand Master Yuki", deck: "elite_water", skill: 11, bio: "Founded the modern dueling system" }, { name: "Festival King Malik", deck: "elite_lightning", skill: 12, bio: "Undefeated in major tournaments" }, // Legendary tier { name: "Mythic Warrior Astrid", deck: "legendary_mixed", skill: 13, bio: "Wields cards from the ancient era" }, { name: "Cosmic Emperor Vex", deck: "legendary_dark", skill: 14, bio: "Rumored to have supernatural powers" }, { name: "Ultimate Dragon Kai", deck: "legendary_supreme", skill: 15, bio: "The strongest duelist alive" }, { name: "Infinity Master Zen", deck: "legendary_infinite", skill: 16, bio: "Has never lost a single duel" } ]; function generateDuelistDeck(deckType, skillLevel) { const starRange = Math.min(10, Math.floor(skillLevel / 2) + 1); const elementTypes = Object.keys(ELEMENT_CYCLE); let deckCards = []; if (deckType.includes("basic")) { const element = deckType.split("_")[1]; deckCards = [ ...shuffle(MONSTERS.filter(m => m.element === element && m.stars <= 3)).slice(0, 15), ...shuffle(MAGIC_CARDS.filter(c => c.rarity === "common")).slice(0, 8), ...shuffle(TRAP_CARDS.filter(c => c.rarity === "common")).slice(0, 7) ]; } else if (deckType.includes("improved")) { const element = deckType.split("_")[1]; deckCards = [ ...shuffle(MONSTERS.filter(m => m.element === element && m.stars <= 5)).slice(0, 18), ...shuffle(MAGIC_CARDS.filter(c => c.rarity !== "super_rare")).slice(0, 7), ...shuffle(TRAP_CARDS.filter(c => c.rarity !== "super_rare")).slice(0, 5) ]; } else if (deckType.includes("advanced")) { const element = deckType === "advanced_mixed" ? elementTypes[Math.floor(Math.random() * elementTypes.length)] : deckType.split("_")[1]; deckCards = [ ...shuffle(MONSTERS.filter(m => m.stars <= 7 && (deckType === "advanced_mixed" || m.element === element))).slice(0, 20), ...shuffle(MAGIC_CARDS.filter(c => c.rarity !== "common")).slice(0, 6), ...shuffle(TRAP_CARDS).slice(0, 4) ]; } else if (deckType.includes("expert")) { deckCards = [ ...shuffle(MONSTERS.filter(m => m.stars >= 5 && m.stars <= 9)).slice(0, 20), ...shuffle(MAGIC_CARDS.filter(c => c.rarity === "rare" || c.rarity === "super_rare")).slice(0, 6), ...shuffle(TRAP_CARDS.filter(c => c.rarity !== "common")).slice(0, 4) ]; } else if (deckType.includes("master")) { deckCards = [ ...shuffle(MONSTERS.filter(m => m.stars >= 6)).slice(0, 22), ...shuffle(MAGIC_CARDS.filter(c => c.rarity === "super_rare")).slice(0, 5), ...shuffle(TRAP_CARDS.filter(c => c.rarity === "rare" || c.rarity === "super_rare")).slice(0, 3) ]; } else if (deckType.includes("elite")) { deckCards = [ ...shuffle(MONSTERS.filter(m => m.stars >= 7)).slice(0, 23), ...shuffle(MAGIC_CARDS.filter(c => c.rarity === "super_rare")).slice(0, 5), ...shuffle(TRAP_CARDS.filter(c => c.rarity === "super_rare")).slice(0, 2) ]; } else { // legendary deckCards = [ ...shuffle(MONSTERS.filter(m => m.stars >= 8)).slice(0, 25), ...shuffle(MAGIC_CARDS.filter(c => c.rarity === "super_rare")).slice(0, 3), ...shuffle(TRAP_CARDS.filter(c => c.rarity === "super_rare")).slice(0, 2) ]; } return shuffle(deckCards).slice(0, 30); } function TournamentScreen({ playerGold, onRegister, activeTournamentData, onStartMatch }) { const [selectedTournament, setSelectedTournament] = useState(null); const difficultyColors = { "Beginner": "#4caf50", "Easy": "#8bc34a", "Medium": "#ffc107", "Hard": "#ff9800", "Expert": "#ff5722", "Elite": "#e91e63", "Legendary": "#9c27b0" }; if (activeTournamentData) { // Show bracket const bracket = activeTournamentData.bracket; const currentMatch = activeTournamentData.currentMatch; return (

šŸ† {activeTournamentData.tournament.name}

Round {activeTournamentData.currentRound} / {Math.log2(activeTournamentData.tournament.participants)}
PRIZE GOLD
šŸ’° {activeTournamentData.tournament.prizeGold}g
{activeTournamentData.tournament.prizeCards > 0 && (
PRIZE CARDS
šŸŽ“ {activeTournamentData.tournament.prizeCards} cards
)}
{/* Tournament Bracket */}

Full Tournament Bracket

{(() => { const totalRounds = Math.log2(activeTournamentData.tournament.participants); const allRounds = []; // Build all rounds structure let participants = activeTournamentData.tournament.participants; for (let round = 1; round <= totalRounds; round++) { const matchesInRound = participants / 2; allRounds.push({ round, matches: matchesInRound }); participants = matchesInRound; } return allRounds.map((roundInfo, roundIdx) => (
{roundInfo.round === totalRounds ? "FINAL" : roundInfo.round === totalRounds - 1 ? "SEMI-FINAL" : `ROUND ${roundInfo.round}`}
{Array.from({ length: roundInfo.matches }).map((_, matchIdx) => { const isCurrentRound = roundInfo.round === activeTournamentData.currentRound; const match = isCurrentRound && bracket[matchIdx] ? bracket[matchIdx] : null; const isCompleted = match?.completed; const isActive = isCurrentRound && matchIdx === currentMatch; return (
{match ? ( <>
{match.player1 === "YOU" ? "šŸ‘¤ YOU" : match.player1?.substring(0, 15) || "TBD"}
{match.player2 === "YOU" ? "šŸ‘¤ YOU" : match.player2?.substring(0, 15) || "TBD"}
{isCompleted && (
Winner: {match.winner === "YOU" ? "YOU" : match.winner?.substring(0, 12)}
)} ) : (
TBD
)}
); })}
)); })()}

Current Round Details - Round {activeTournamentData.currentRound}

{bracket.map((match, i) => { const isCurrentMatch = currentMatch === i; const isCompleted = match.winner !== null; const isInProgress = match.inProgress === true; return (
{i === currentMatch && !isCompleted && !isInProgress && (
*NEW*
)}
{match.player1 === "YOU" ? "šŸ‘¤ YOU" : `šŸŽ“ ${match.player1}`}
{match.player2 === "YOU" ? "šŸ‘¤ YOU" : `šŸŽ“ ${match.player2}`}
{isCompleted ? (
āœ“ {match.winner === "YOU" ? "YOU WON" : `${match.winner} WON`}
) : isInProgress ? (
āš”ļø IN PROGRESS
) : isCurrentMatch ? ( ) : (
Waiting...
)}
); })}
); } return (

šŸ† Tournaments

Choose a tournament and test your skills against unique duelists!

{TOURNAMENTS.map(tournament => { const canAfford = playerGold >= tournament.entryFee; const diffColor = difficultyColors[tournament.difficulty]; return (
canAfford && setSelectedTournament(tournament)}>

{tournament.name}

{tournament.difficulty}
ENTRY FEE
{tournament.entryFee === 0 ? "FREE" : `${tournament.entryFee}g`}
PARTICIPANTS
{tournament.participants}
PRIZES
{tournament.prizeGold > 0 && (
šŸ’° {tournament.prizeGold} gold
)} {tournament.prizeCards > 0 && (
šŸŽ“ {tournament.prizeCards} random cards
)}
{!canAfford && (
āŒ Need {tournament.entryFee - playerGold} more gold
)}
); })}
{selectedTournament && (
)}
); } // ============================================================ // BATTLE SCREEN WITH PLAYABLE FIELD // ============================================================ function BattleScreen({ inBattle, playerDeck, playerName, onBattleEnd, currentBattleInfo }) { const [battleState, setBattleState] = useState(null); const [selectedCard, setSelectedCard] = useState(null); const [selectedTarget, setSelectedTarget] = useState(null); const [battleLog, setBattleLog] = useState([]); const [viewingGraveyard, setViewingGraveyard] = useState(null); const battleLogRef = useRef(null); const battleLogRef = useRef(null); useEffect(() => { if (inBattle && !battleState) { initBattle(); } }, [inBattle]); useEffect(() => { if (battleLogRef.current) { battleLogRef.current.scrollTop = battleLogRef.current.scrollHeight; } }, [battleLog]); function initBattle() { const shuffledDeck = shuffle([...playerDeck]); const playerHand = shuffledDeck.slice(0, 5); const playerDrawPile = shuffledDeck.slice(5); const opponentDeck = currentBattleInfo?.opponentDeck || buildStarterDeck(); const shuffledOppDeck = shuffle([...opponentDeck]); const opponentHand = shuffledOppDeck.slice(0, 5); const opponentDrawPile = shuffledOppDeck.slice(5); setBattleState({ player: { hp: 8000, hand: playerHand, monsterField: [], spellTrapField: [], graveyard: [], drawPile: playerDrawPile }, opponent: { hp: 8000, hand: opponentHand, monsterField: [], spellTrapField: [], graveyard: [], drawPile: opponentDrawPile, name: currentBattleInfo?.opponent?.name || "Opponent" }, turn: "player", turnCount: 1, phase: "draw" }); addBattleLog("āš”ļø DUEL START! Draw your opening hand!"); } function addBattleLog(msg) { setBattleLog(prev => [...prev, { text: msg, id: Date.now() + Math.random() }]); } function drawCard(side) { setBattleState(prev => { const newState = {...prev}; const sideData = newState[side]; if (sideData.drawPile.length === 0) { addBattleLog(`${side === 'player' ? playerName : 'Opponent'} has no cards left to draw!`); return prev; } const drawnCard = sideData.drawPile[0]; sideData.hand.push(drawnCard); sideData.drawPile = sideData.drawPile.slice(1); addBattleLog(`${side === 'player' ? playerName : 'Opponent'} draws a card.`); return newState; }); } function playCard(card, side) { setBattleState(prev => { const newState = {...prev}; const sideData = newState[side]; // Check field limits if (card.type === "monster") { if (sideData.monsterField.length >= 5) { addBattleLog(`Monster field is full! (5 card limit)`); return prev; } // Check if can play (monster needs sacrifices) if (card.sacrifices > 0 && sideData.monsterField.length < card.sacrifices) { addBattleLog(`Need ${card.sacrifices} sacrifice(s) to summon ${card.name}!`); return prev; } // Handle sacrifices if (card.sacrifices > 0) { const sacrificed = sideData.monsterField.slice(0, card.sacrifices); sideData.monsterField = sideData.monsterField.slice(card.sacrifices); sideData.graveyard.push(...sacrificed); addBattleLog(`Sacrificed ${sacrificed.map(c => c.name).join(", ")} to summon ${card.name}!`); } // Remove from hand sideData.hand = sideData.hand.filter(c => c !== card); sideData.monsterField.push({...card, currentHp: card.hp, canAttack: false}); addBattleLog(`${side === 'player' ? playerName : newState.opponent.name} summons ${card.name}!`); } else { // Magic/Trap if (sideData.spellTrapField.length >= 5) { addBattleLog(`Spell/Trap field is full! (5 card limit)`); return prev; } // Remove from hand sideData.hand = sideData.hand.filter(c => c !== card); // Set face-down sideData.spellTrapField.push({...card, faceDown: true}); addBattleLog(`${side === 'player' ? playerName : newState.opponent.name} sets a ${card.type} card face-down.`); } return newState; }); setSelectedCard(null); } function attack(attackerIndex) { if (!selectedTarget && selectedTarget !== 0) { addBattleLog("Select a target to attack!"); return; } setBattleState(prev => { const newState = {...prev}; // Prevent first turn attacks if (newState.turnCount === 1 && newState.turn === "player") { addBattleLog(`Cannot attack on the first turn!`); return prev; } const attacker = newState.player.monsterField[attackerIndex]; if (!attacker.canAttack) { addBattleLog(`${attacker.name} cannot attack this turn!`); return prev; } if (selectedTarget === "direct") { // Direct attack const damage = attacker.atk; newState.opponent.hp -= damage; addBattleLog(`${attacker.name} attacks directly for ${damage} damage!`); } else { // Attack monster const defender = newState.opponent.monsterField[selectedTarget]; const atkDamage = attacker.atk; const defDamage = defender.atk; defender.currentHp -= atkDamage; attacker.currentHp -= defDamage; addBattleLog(`${attacker.name} (${attacker.atk} ATK) battles ${defender.name} (${defender.atk} ATK)!`); // Check destruction if (defender.currentHp <= 0) { newState.opponent.monsterField = newState.opponent.monsterField.filter((_, i) => i !== selectedTarget); newState.opponent.graveyard.push(defender); addBattleLog(`${defender.name} is destroyed!`); } if (attacker.currentHp <= 0) { newState.player.monsterField = newState.player.monsterField.filter((_, i) => i !== attackerIndex); newState.player.graveyard.push(attacker); addBattleLog(`${attacker.name} is destroyed!`); } } attacker.canAttack = false; setSelectedTarget(null); return newState; }); } function endTurn() { setBattleState(prev => { const newState = {...prev}; if (newState.turn === "player") { // Refresh monsters newState.player.monsterField.forEach(m => m.canAttack = true); newState.turn = "opponent"; newState.turnCount++; addBattleLog("šŸ”„ Opponent's turn!"); // Simple AI setTimeout(() => aiTurn(newState), 1000); } else { newState.opponent.monsterField.forEach(m => m.canAttack = true); newState.turn = "player"; newState.turnCount++; addBattleLog("šŸ”„ Your turn! Draw a card."); drawCard("player"); } return newState; }); } function aiTurn(state) { // Simple AI if (state.opponent.hand.length > 0) { const card = state.opponent.hand[0]; playCard(card, "opponent"); } setTimeout(() => { setBattleState(prev => { const newState = {...prev}; // Attack with first monster if available and not first turn if (newState.opponent.monsterField.length > 0 && newState.turnCount > 1) { const attacker = newState.opponent.monsterField[0]; if (newState.player.monsterField.length > 0) { // Attack player's first monster const defender = newState.player.monsterField[0]; defender.currentHp -= attacker.atk; attacker.currentHp -= defender.atk; addBattleLog(`${newState.opponent.name}'s ${attacker.name} attacks ${defender.name}!`); if (defender.currentHp <= 0) { newState.player.monsterField.shift(); newState.player.graveyard.push(defender); addBattleLog(`${defender.name} destroyed!`); } if (attacker.currentHp <= 0) { newState.opponent.monsterField.shift(); newState.opponent.graveyard.push(attacker); addBattleLog(`${attacker.name} destroyed!`); } } else { // Direct attack newState.player.hp -= attacker.atk; addBattleLog(`${newState.opponent.name}'s ${attacker.name} attacks directly for ${attacker.atk} damage!`); } } return newState; }); setTimeout(() => endTurn(), 1000); }, 1500); } if (!inBattle || !battleState) { return (
āš”ļø

No Active Battle

Accept a duel in Story mode to begin!

); } // Check win conditions if (battleState.player.hp <= 0) { return (

šŸ’€ DEFEAT

You lost the duel...

); } if (battleState.opponent.hp <= 0) { return (

šŸ† VICTORY!

You won the duel and earned 50 gold!

); } return (
{/* HP Bars and Counters */}
OPPONENT
ā¤ļø {battleState.opponent.hp}
setViewingGraveyard({side:"opponent", cards:battleState.opponent.graveyard})} style={{ cursor:"pointer", textAlign:"center", background:"rgba(155,89,182,0.2)", padding:"8px 12px", borderRadius:6, border:"1px solid #9b59b6" }}>
GRAVEYARD
šŸ’€ {battleState.opponent.graveyard.length}
DECK
šŸŽ“ {battleState.opponent.drawPile.length}
{battleState.turn === "player" ? "YOUR TURN" : "OPPONENT'S TURN"}
Turn {battleState.turnCount}
DECK
šŸŽ“ {battleState.player.drawPile.length}
setViewingGraveyard({side:"player", cards:battleState.player.graveyard})} style={{ cursor:"pointer", textAlign:"center", background:"rgba(155,89,182,0.2)", padding:"8px 12px", borderRadius:6, border:"1px solid #9b59b6" }}>
GRAVEYARD
šŸ’€ {battleState.player.graveyard.length}
{playerName.toUpperCase()}
ā¤ļø {battleState.player.hp}
{/* Opponent Monster Field */}
OPPONENT MONSTERS ({battleState.opponent.monsterField.length}/5)
{battleState.opponent.monsterField.map((card, i) => (
battleState.turn === "player" && setSelectedTarget(i)} style={{ cursor: battleState.turn === "player" ? "pointer" : "default", opacity: selectedTarget === i ? 1 : 0.8 }}>
{card.currentHp} HP
))} {battleState.turn === "player" && battleState.opponent.monsterField.length === 0 && (
setSelectedTarget("direct")} style={{ padding:"20px 40px", background:"rgba(255,255,255,0.05)", border:"2px dashed #666", borderRadius:8, cursor:"pointer", display:"flex", alignItems:"center", justifyContent:"center" }}> DIRECT ATTACK
)}
{/* Opponent Spell/Trap Field */}
OPPONENT SPELL/TRAP ({battleState.opponent.spellTrapField.length}/5)
{battleState.opponent.spellTrapField.map((card, i) => (
{card.faceDown ? "šŸ‚ " : card.type === "magic" ? "✨" : "⚔"}
))}
{/* Battle Log */}
{battleLog.slice(-5).map(log => (
{log.text}
))}
{/* Player Spell/Trap Field */}
YOUR SPELL/TRAP ({battleState.player.spellTrapField.length}/5)
{battleState.player.spellTrapField.map((card, i) => (
{card.faceDown ? "šŸ‚ " : ( <> {card.type === "magic" ? "✨" : "⚔"} {card.name.substring(0,8)} )}
))}
{/* Player Monster Field */}
YOUR MONSTERS ({battleState.player.monsterField.length}/5)
{battleState.player.monsterField.map((card, i) => (
{card.currentHp} HP
{battleState.turn === "player" && card.canAttack && battleState.turnCount > 1 && ( )} {battleState.turn === "player" && battleState.turnCount === 1 && (
Turn 1
)}
))}
{/* Player Hand */}
YOUR HAND ({battleState.player.hand.length})
{battleState.turn === "player" && ( )}
{battleState.player.hand.map((card, i) => (
battleState.turn === "player" && playCard(card, "player")} style={{ cursor: battleState.turn === "player" ? "pointer" : "not-allowed" }}>
))}
{/* Graveyard Viewer Modal */} {viewingGraveyard && (
setViewingGraveyard(null)}>
e.stopPropagation()} style={{ background:"#1a1a1a", border:"2px solid #9b59b6", borderRadius:12, padding:24, maxWidth:800, maxHeight:"80vh", overflowY:"auto" }}>

šŸ’€ {viewingGraveyard.side === "player" ? "Your" : "Opponent's"} Graveyard ({viewingGraveyard.cards.length} cards)

{viewingGraveyard.cards.length === 0 ? (
No cards in graveyard
) : (
{viewingGraveyard.cards.map((card, i) => ( ))}
)}
)}
); } // ============================================================ // MAIN APP // ============================================================ export default function ShadowRealmApp() { const [screen, setScreen] = useState("title"); const [playerName, setPlayerName] = useState(""); const [nameInput, setNameInput] = useState(""); const [playerDeck, setPlayerDeck] = useState([]); const [playerCollection, setPlayerCollection] = useState([]); const [playerGold, setPlayerGold] = useState(100); const [activeTab, setActiveTab] = useState("tournament"); const [inBattle, setInBattle] = useState(false); const [activeTournamentData, setActiveTournamentData] = useState(null); const [currentBattleInfo, setCurrentBattleInfo] = useState(null); useEffect(() => { const saved = loadGame(); if (saved?.playerName) { setPlayerName(saved.playerName); setPlayerDeck(saved.playerDeck||[]); setPlayerCollection(saved.playerCollection||[]); setPlayerGold(saved.playerGold||100); setActiveTournamentData(saved.activeTournamentData||null); setScreen("main"); } }, []); useEffect(() => { if (playerName) saveGame({ playerName, playerDeck, playerCollection, playerGold, activeTournamentData }); }, [playerName, playerGold, playerDeck, playerCollection, activeTournamentData]); function startGame() { if (!nameInput.trim()) return; setPlayerName(nameInput.trim()); const deck = buildStarterDeck(); setPlayerDeck(deck); setPlayerCollection(deepClone(deck)); setStoryState({ log: [{ role:"narrator", text:`You step into the dimly lit card shop, the scent of old paper and ink filling your nostrils. A challenger approaches with a confident smirk. "Hey, you look like you know your way around a deck. How about a friendly duel? Winner takes 50 gold."` }], currentEvent: STORY_EVENTS[0], showTournamentButton: false }); setScreen("main"); } function handleShopBuy(cost, cards) { setPlayerGold(g => g - cost); setPlayerCollection(c => [...c, ...cards]); } function handleSell(card, sellPrice) { const key = getCardKey(card); let removed = false; const newCollection = []; for (const c of playerCollection) { if (!removed && getCardKey(c) === key) { removed = true; continue; } newCollection.push(c); } setPlayerCollection(newCollection); setPlayerGold(g => g + sellPrice); } function handleStartTournament() { setTournamentActive(true); setActiveTab("tournament"); } function handleStartBattle(battleInfo) { console.log("šŸŽ® Starting battle..."); setCurrentBattleInfo(battleInfo); setInBattle(true); setTimeout(() => { console.log("āš”ļø Switching to battle tab"); setActiveTab("battle"); }, 100); } function handleBattleEnd(won) { setInBattle(false); if (activeTournamentData && currentBattleInfo) { const newTournamentData = {...activeTournamentData}; const match = newTournamentData.bracket[newTournamentData.currentMatch]; match.winner = won ? "YOU" : (match.player1 === "YOU" ? match.player2 : match.player1); match.completed = true; match.inProgress = false; // Clear in progress flag const allMatchesCompleted = newTournamentData.bracket.every(m => m.completed); if (allMatchesCompleted) { if (match.winner === "YOU") { const prize = newTournamentData.tournament; setPlayerGold(g => g + prize.prizeGold); if (prize.prizeCards > 0) { const prizeCards = shuffle(ALL_CARDS).slice(0, prize.prizeCards); setPlayerCollection(c => [...c, ...prizeCards]); } alert(`šŸ† TOURNAMENT VICTORY!\n\nšŸ’° Won ${prize.prizeGold} gold!\nšŸŽ“ Won ${prize.prizeCards} cards!`); } else { alert(`šŸ’€ Tournament Eliminated\n\nBetter luck next time!`); } setActiveTournamentData(null); setActiveTab("tournament"); } else { newTournamentData.currentMatch++; if (newTournamentData.currentMatch >= newTournamentData.bracket.length) { const winners = newTournamentData.bracket.filter(m => m.completed).map(m => m.winner); if (winners.length > 1) { const nextRoundBracket = []; for (let i = 0; i < winners.length; i += 2) { nextRoundBracket.push({ player1: winners[i], player2: winners[i + 1] || winners[i], winner: null, completed: false }); } newTournamentData.bracket = nextRoundBracket; newTournamentData.currentMatch = 0; newTournamentData.currentRound++; } } setActiveTournamentData(newTournamentData); setActiveTab("tournament"); } } else { if (won) { setPlayerGold(g => g + 50); } setActiveTab("tournament"); } setCurrentBattleInfo(null); } function handleTournamentRegister(tournament) { if (playerGold < tournament.entryFee) { alert("Not enough gold!"); return; } setPlayerGold(g => g - tournament.entryFee); const duelists = shuffle(DUELISTS).slice(0, tournament.participants - 1); const participants = ["YOU", ...duelists.map(d => d.name)]; const shuffledParticipants = shuffle(participants); const bracket = []; for (let i = 0; i < shuffledParticipants.length; i += 2) { bracket.push({ player1: shuffledParticipants[i], player2: shuffledParticipants[i + 1], winner: null, completed: false }); } setActiveTournamentData({ tournament, bracket, duelists, currentMatch: 0, currentRound: 1 }); } function handleStartMatch(match, matchIndex) { // Mark match as in progress const newTournamentData = {...activeTournamentData}; newTournamentData.bracket[matchIndex].inProgress = true; setActiveTournamentData(newTournamentData); const opponent = match.player1 === "YOU" ? match.player2 : match.player1; const duelistData = DUELISTS.find(d => d.name === opponent); handleStartBattle({ type: "tournament", opponent: duelistData, opponentDeck: generateDuelistDeck(duelistData.deck, duelistData.skill), matchIndex }); } const TABS = [ { id:"deck", label:"Deck", icon:"šŸƒ" }, { id:"collection", label:"Collection", icon:"šŸŽ“" }, { id:"codex", label:"Codex", icon:"šŸ“š" }, { id:"shop", label:"Shop", icon:"šŸŖ" }, { id:"elements", label:"Elements", icon:"⚔" }, { id:"tournament", label:"Tournaments", icon:"šŸ†" }, { id:"battle", label:"Battle", icon:"āš”ļø", disabled: !inBattle }, ]; if (screen === "title") { return (
šŸŒ™

DUEL MASTERS

1000 CARDS • 8 ELEMENTS

Enter your name, Duelist

setNameInput(e.target.value)} onKeyDown={e=>e.key==="Enter"&&startGame()} placeholder="Your name..." style={{ background:"rgba(255,255,255,0.05)", border:"2px solid #9b59b6", borderRadius:10, padding:"12px 16px", color:"#fff", width:"100%", fontSize:16, marginBottom:16, outline:"none" }} />
); } return (
šŸŒ™ DUEL MASTERS
{TABS.map(tab => ( ))}
šŸ’° {playerGold}g šŸ‘¤ {playerName}
{activeTab === "deck" && } {activeTab === "collection" && } {activeTab === "codex" && } {activeTab === "shop" && } {activeTab === "elements" && } {activeTab === "tournament" && } {activeTab === "battle" && }
); }