DUEL MASTERS
by FrostCobra302154 lines105.1 KB
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 (
<div
onMouseEnter={() => showHoverPreview && setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ position: "relative", display: "inline-block" }}
>
<div onClick={onClick} style={{
width:w, height:h, background: notOwned ? "#111" : "#1a1a1a",
border:`2px solid ${selected?"#ffd700":typeColor}`, borderRadius:8,
cursor:onClick?"pointer":"default", display:"flex",flexDirection:"column",
position:"relative", opacity: notOwned ? 0.3 : 1,
boxShadow: selected?`0 0 20px #ffd70099`:`0 2px 8px rgba(0,0,0,0.5)`,
flexShrink:0,
transition: "all 0.3s ease, transform 0.2s ease",
transform: selected ? "translateY(-5px)" : "translateY(0)"
}}>
<div style={{ background:typeColor, padding:small?"3px 6px":"4px 8px", display:"flex", justifyContent:"space-between" }}>
<span style={{ fontSize:small?5:7, color:"#fff", fontWeight:700, fontFamily:"monospace" }}>{card.cardNumber}</span>
{card.type==="monster" && <span style={{ fontSize:small?7:9, color:"#ffd700" }}>{"★".repeat(Math.min(card.stars,5))}</span>}
</div>
<div style={{ padding:small?"4px 5px":"5px 8px", borderBottom:`1px solid ${typeColor}33` }}>
<div style={{ fontSize:small?7:9, color:"#fff", fontWeight:700, textAlign:"center", lineHeight:1.2 }}>{card.name}</div>
</div>
<div style={{ fontSize:small?18:28, textAlign:"center", padding:small?"4px 0":"6px 0", lineHeight:1 }}>
{card.type==="monster"?"🌙":card.type==="magic"?"✨":"⚡"}
</div>
{card.type==="monster" && (
<div style={{ display:"flex", justifyContent:"space-around", padding:small?"2px":"3px 6px", background:"rgba(0,0,0,0.3)", borderTop:"1px solid rgba(255,255,255,0.1)" }}>
{[["ATK",card.atk],["DEF",card.def],["HP",card.hp]].map(([lbl,val])=>(
<div key={lbl} style={{ textAlign:"center" }}>
<div style={{ fontSize:small?4:5, color:"#888" }}>{lbl}</div>
<div style={{ fontSize:small?8:10, color:lbl==="HP"?"#2ecc71":"#fff", fontWeight:700 }}>{val}</div>
</div>
))}
</div>
)}
{!small && (
<div style={{ padding:"4px 6px", fontSize:6, flex:1, overflow:"hidden", lineHeight:1.3 }}>
{card.type==="monster" ? (
<>
{elInfo && <span style={{ fontSize:8, marginRight:3 }}>{elInfo.icon}</span>}
{card.effect ? <span style={{ color:"#f1c40f" }}>{card.effect.desc}</span> : <span style={{ color:"#aaa" }}>{card.desc}</span>}
</>
) : (
<span style={{ color:"#aaa" }}>{card.effectDesc || card.desc}</span>
)}
</div>
)}
<div style={{ background:"rgba(0,0,0,0.5)", padding:small?"2px 4px":"3px 6px", display:"flex", justifyContent:"space-between", borderTop:"1px solid rgba(255,255,255,0.1)" }}>
<span style={{ fontSize:small?4:6, color:RARITY_COLORS[card.rarity], fontWeight:700, textTransform:"uppercase" }}>{card.rarity?.replace("_"," ")}</span>
{elInfo && <span style={{ fontSize:small?4:6, color:elInfo.color, fontWeight:700, textTransform:"uppercase" }}>{card.element}</span>}
</div>
{quantity > 1 && <div style={{ position:"absolute", top:4, left:4, background:"rgba(0,0,0,0.95)", border:"2px solid #f39c12", padding:"4px 8px", borderRadius:6, fontSize:small?9:12, color:"#f1c40f", fontWeight:700, boxShadow:"0 2px 8px rgba(0,0,0,0.6)", zIndex:10 }}>×{quantity}</div>}
{inDeckCount > 0 && <div style={{ position:"absolute", top:4, right:4, background:"rgba(155,89,182,0.95)", border:"2px solid #9b59b6", padding:"4px 8px", borderRadius:6, fontSize:small?9:12, color:"#fff", fontWeight:700, boxShadow:"0 2px 8px rgba(0,0,0,0.6)", zIndex:10 }}>📋 {inDeckCount}</div>}
{isNew && <div className="bounce" style={{ position:"absolute", top:small?-8:-12, right:small?-8:-12, background:"#f39c12", border:"2px solid #fff", padding:"4px 8px", borderRadius:12, fontSize:small?8:10, color:"#000", fontWeight:900, boxShadow:"0 3px 12px rgba(243,156,18,0.8)", zIndex:20, transform:"rotate(15deg)" }}>*NEW*</div>}
{notOwned && <div style={{ position:"absolute", inset:0, background:"rgba(0,0,0,0.7)", borderRadius:8, display:"flex",alignItems:"center",justifyContent:"center", fontSize:small?16:24 }}>🔒</div>}
</div>
{hovered && showHoverPreview && !selected && (
<div style={{ position: "fixed", left: "50%", top: "50%", transform: "translate(-50%, -50%)", zIndex: 10000, pointerEvents: "none" }}>
<EnlargedCard card={card} />
</div>
)}
</div>
);
}
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 (
<div style={{ width: 300, height: 420, background: "#1a1a1a", border: `4px solid ${typeColor}`, borderRadius: 16, boxShadow: "0 8px 32px rgba(0,0,0,0.8)", display: "flex", flexDirection: "column", overflow: "hidden" }}>
<div style={{ background: typeColor, padding: "12px", display: "flex", justifyContent: "space-between" }}>
<span style={{ fontSize: 14, color: "#fff", fontWeight: 700, fontFamily: "monospace" }}>{card.cardNumber}</span>
{card.type === "monster" && <span style={{ fontSize: 18, color: "#ffd700" }}>{"★".repeat(Math.min(card.stars, 5))}</span>}
</div>
<div style={{ padding: "16px", borderBottom: `2px solid ${typeColor}` }}>
<div style={{ fontSize: 20, color: "#fff", fontWeight: 700, textAlign: "center", marginBottom: 8 }}>{card.name}</div>
<div style={{ fontSize: 12, color: "#aaa", textAlign: "center" }}>{card.type.toUpperCase()}</div>
</div>
<div style={{ fontSize: 60, textAlign: "center", padding: "20px 0" }}>
{card.type === "monster" ? "🌙" : card.type === "magic" ? "✨" : "⚡"}
</div>
{card.type === "monster" && (
<div style={{ display: "flex", justifyContent: "space-around", padding: "12px", background: "rgba(0,0,0,0.3)", borderTop: "1px solid rgba(255,255,255,0.1)", borderBottom: "1px solid rgba(255,255,255,0.1)" }}>
{[["ATK", card.atk], ["DEF", card.def], ["HP", card.hp]].map(([lbl, val]) => (
<div key={lbl} style={{ textAlign: "center" }}>
<div style={{ fontSize: 11, color: "#888", marginBottom: 4 }}>{lbl}</div>
<div style={{ fontSize: 20, color: lbl === "HP" ? "#2ecc71" : "#fff", fontWeight: 700 }}>{val}</div>
</div>
))}
</div>
)}
<div style={{ padding: "16px", flex: 1, overflow: "auto" }}>
{card.effect ? (
<>
<div style={{ fontSize: 12, color: "#f1c40f", fontWeight: 600, padding: "8px", background: "rgba(241,196,15,0.1)", borderRadius: 6, lineHeight: 1.6, marginBottom: 12 }}>
<div style={{ color: "#f39c12", fontSize: 10, marginBottom: 4, textTransform: "uppercase", letterSpacing: 1 }}>⚡ CARD EFFECT:</div>
{card.effect.desc}
</div>
<div style={{ fontSize: 10, color: "#666", lineHeight: 1.6 }}>
{elInfo && <span style={{ fontSize: 14, marginRight: 6 }}>{elInfo.icon}</span>}
{card.desc}
</div>
</>
) : card.effectDesc && card.type !== "monster" ? (
<>
<div style={{ fontSize: 12, color: "#4a90e2", fontWeight: 600, padding: "8px", background: "rgba(74,144,226,0.1)", borderRadius: 6, lineHeight: 1.6, marginBottom: 12 }}>
<div style={{ color: "#3498db", fontSize: 10, marginBottom: 4, textTransform: "uppercase", letterSpacing: 1 }}>⚡ CARD EFFECT:</div>
{card.effectDesc}
</div>
<div style={{ fontSize: 10, color: "#666", lineHeight: 1.6 }}>
{card.desc}
</div>
</>
) : (
<div style={{ fontSize: 11, color: "#aaa", lineHeight: 1.6 }}>
{elInfo && <span style={{ fontSize: 16, marginRight: 6 }}>{elInfo.icon}</span>}
{card.desc}
</div>
)}
</div>
<div style={{ background: "rgba(0,0,0,0.5)", padding: "12px", display: "flex", justifyContent: "space-between", borderTop: "1px solid rgba(255,255,255,0.1)" }}>
<span style={{ fontSize: 12, color: RARITY_COLORS[card.rarity], fontWeight: 700, textTransform: "uppercase" }}>{card.rarity?.replace("_", " ")}</span>
{elInfo && <span style={{ fontSize: 12, color: elInfo.color, fontWeight: 700, textTransform: "uppercase" }}>{card.element}</span>}
</div>
</div>
);
}
// ============================================================
// PINNED CARD PREVIEW (CLICK TO PIN)
// ============================================================
function PinnedCardPreview({ card, onAddToDeck, onClose }) {
if (!card) return null;
return (
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.8)", zIndex: 10000, display: "flex", alignItems: "center", justifyContent: "center" }} onClick={onClose}>
<div onClick={(e) => e.stopPropagation()} style={{ width: 300 }}>
<EnlargedCard card={card} />
<div style={{ padding: "16px", display: "flex", gap: 12, background: "#1a1a1a", borderRadius: "0 0 16px 16px" }}>
<button onClick={onAddToDeck} style={{
flex: 1, background: "#4caf50", border: "none", borderRadius: 8, padding: "12px",
color: "#fff", fontWeight: 700, fontSize: 14, cursor: "pointer"
}}>✓ Add to Deck</button>
<button onClick={onClose} style={{
flex: 1, background: "#e74c3c", border: "none", borderRadius: 8, padding: "12px",
color: "#fff", fontWeight: 700, fontSize: 14, cursor: "pointer"
}}>✕ Cancel</button>
</div>
</div>
</div>
);
}
// ============================================================
// 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 (
<div style={{ display:"flex", flexDirection:"column", height:"calc(100vh - 52px)", padding:"20px 30px", maxWidth:800, margin:"0 auto" }}>
<h2 style={{ color:"#9b59b6", fontSize:28, marginBottom:16 }}>📖 Story Mode</h2>
<div ref={logRef} style={{
flex:1, overflowY:"auto",
background:"rgba(0,0,0,0.4)", border:"1px solid rgba(255,255,255,0.08)",
borderRadius:12, padding:20, marginBottom:16,
display:"flex", flexDirection:"column", gap:12
}}>
{storyLog.map((entry,i) => (
<div key={entry.id||i} className="fade-in" style={{
background: entry.role==="player" ? "rgba(155,89,182,0.15)" : "rgba(255,255,255,0.03)",
border: `1px solid ${entry.role==="player"?"rgba(155,89,182,0.3)":"rgba(255,255,255,0.06)"}`,
borderRadius:8, padding:"10px 14px"
}}>
<span style={{ fontSize:10, color:entry.role==="player"?"#9b59b6":"#888", fontWeight:700, textTransform:"uppercase", marginRight:8 }}>
{entry.role==="player"?playerName:"Narrator"}
</span>
<span style={{ color: entry.role==="player"?"#e0d0ff":"#ccc", fontSize:13, lineHeight:1.6 }}>{entry.text}</span>
</div>
))}
{loading && <div style={{ color:"#888", fontSize:12, fontStyle:"italic" }}>...</div>}
{inBattle && (
<div style={{ background:"rgba(231,76,60,0.2)", border:"2px solid #e74c3c", borderRadius:8, padding:12, textAlign:"center" }}>
<div style={{ color:"#e74c3c", fontWeight:700 }}>⚔️ BATTLE IN PROGRESS</div>
<div style={{ color:"#888", fontSize:11, marginTop:4 }}>Story input disabled until battle ends</div>
</div>
)}
</div>
{showTournamentButton && !tournamentActive && (
<div style={{ marginBottom:12 }}>
<button onClick={onStartTournament} style={{
width:"100%", background:"#f39c12", border:"none", borderRadius:10, padding:"14px",
color:"#000", fontSize:14, fontWeight:700, cursor:"pointer"
}}>🏆 START TOURNAMENT</button>
</div>
)}
{/* 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")
) && (
<div style={{ marginBottom:12 }}>
<button onClick={() => {
if (window.confirm("⚔️ BEGIN DUEL?\n\n🎴 Your opponent is ready!\n💰 Winner takes 50 gold\n\nClick OK to enter the Battle Arena.")) {
onStartBattle({ type: "friendly", wager: 50 });
}
}} style={{
width:"100%", background:"#e74c3c", border:"none", borderRadius:10, padding:"14px",
color:"#fff", fontSize:14, fontWeight:700, cursor:"pointer", animation:"pulse 2s infinite"
}}>⚔️ BEGIN DUEL</button>
</div>
)}
{!loading && !inBattle && currentEvent && (
<div style={{ display:"flex", flexWrap:"wrap", gap:8, marginBottom:10 }}>
{currentEvent.options.map((opt,i) => (
<button key={i} onClick={()=>handleOption(opt)} style={{
background:"rgba(155,89,182,0.15)", border:"2px solid rgba(155,89,182,0.4)",
borderRadius:8, padding:"8px 14px", color:"#c39bd3",
fontSize:12, cursor:"pointer", fontWeight:600
}}>{opt}</button>
))}
</div>
)}
<div style={{ display:"flex", gap:10 }}>
<input
value={inputText}
onChange={e=>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"
}}
/>
<button onClick={handleFreeInput} disabled={loading||!inputText.trim()||inBattle} style={{
background:inBattle?"#333":"#9b59b6", border:"none", borderRadius:10, padding:"10px 20px",
color:inBattle?"#555":"#fff", fontWeight:700, fontSize:13, cursor:inBattle?"not-allowed":"pointer"
}}>Send</button>
</div>
</div>
);
}
// ============================================================
// 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 (
<div style={{ display:"flex", height:"calc(100vh - 52px)", overflow:"hidden" }}>
<div style={{ flex:1, padding:20, overflowY:"auto", borderRight:"1px solid rgba(255,255,255,0.08)" }}>
<h3 style={{ color:"#9b59b6", marginBottom:12 }}>Collection ({playerCollection.length} cards)</h3>
<div style={{ display:"flex", gap:8, marginBottom:12, flexWrap:"wrap" }}>
{["all","monster","magic","trap"].map(f => (
<button key={f} onClick={()=>setFilter(f)} style={{
background:filter===f?"#9b59b6":"transparent",
border:`1px solid ${filter===f?"#9b59b6":"#444"}`,
borderRadius:6, padding:"4px 12px", color:filter===f?"#fff":"#888",
fontSize:11, cursor:"pointer"
}}>{f.charAt(0).toUpperCase()+f.slice(1)}</button>
))}
<input value={search} onChange={e=>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
}} />
</div>
<div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fill,minmax(90px,1fr))", gap:8, marginBottom:32 }}>
{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 (
<div key={i} onClick={() => setPinnedCard(card)} style={{ position: "relative" }}>
<CardDisplay
card={card}
small
inDeckCount={inDeck}
showHoverPreview={true}
selected={isSelected}
/>
{owned > 1 && <div style={{ position:"absolute", bottom:-8, left:"50%", transform:"translateX(-50%)", background:"rgba(0,0,0,0.95)", border:"2px solid #f39c12", padding:"4px 10px", borderRadius:6, fontSize:11, color:"#f1c40f", fontWeight:700, boxShadow:"0 2px 8px rgba(0,0,0,0.6)", zIndex:10, whiteSpace:"nowrap" }}>×{owned}</div>}
</div>
);
})}
</div>
</div>
<div style={{ width:320, padding:20, overflowY:"auto", background:"rgba(0,0,0,0.3)" }}>
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:16 }}>
<h3 style={{ color:"#f1c40f", fontSize:18, margin:0 }}>Deck ({deck.length}/30)</h3>
<button onClick={()=>onSave(deck)} style={{ background:"#9b59b6", border:"none", borderRadius:8, padding:"8px 16px", color:"#fff", fontWeight:700, cursor:"pointer" }}>SAVE</button>
</div>
{deck.length === 0 && <p style={{ color:"#555", fontSize:12 }}>Click cards to add</p>}
<div style={{ display:"flex", flexDirection:"column", gap:4 }}>
{deck.map((card,i) => (
<div key={i} style={{ display:"flex", alignItems:"center", gap:8, background:"rgba(255,255,255,0.03)", borderRadius:6, padding:"8px 12px" }}>
<span style={{ fontSize:18 }}>{card.type==="monster"?"🌙":card.type==="magic"?"✨":"⚡"}</span>
<div style={{ flex:1 }}>
<div style={{ color:"#fff", fontSize:12, fontWeight:600 }}>{card.name}</div>
<div style={{ color:"#666", fontSize:9 }}>{card.cardNumber}</div>
</div>
{card.type==="monster" && <span style={{ color:"#888", fontSize:10 }}>{card.atk}A</span>}
<button onClick={()=>setDeck(d=>d.filter((_,idx)=>idx!==i))} style={{ background:"transparent", border:"1px solid #e74c3c", borderRadius:4, padding:"2px 6px", color:"#e74c3c", fontSize:10, cursor:"pointer" }}>✕</button>
</div>
))}
</div>
</div>
{pinnedCard && (
<PinnedCardPreview
card={pinnedCard}
onAddToDeck={() => addToDeck(pinnedCard)}
onClose={() => setPinnedCard(null)}
/>
)}
</div>
);
}
// ============================================================
// 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 (
<div style={{ padding:30 }}>
<h2 style={{ color:"#9b59b6", marginBottom:20 }}>🎴 My Collection ({collection.length} cards)</h2>
<div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fill,minmax(125px,1fr))", gap:14 }}>
{uniqueCards.map((card,i) => {
const key = getCardKey(card);
const qty = collectionQty[key] || 0;
return <CardDisplay key={i} card={card} quantity={qty} showHoverPreview={true} />;
})}
</div>
</div>
);
}
// ============================================================
// 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 (
<div style={{ padding:30, maxWidth:1400, margin:"0 auto" }}>
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:20 }}>
<h2 style={{ color:"#9b59b6", fontSize:28, margin:0 }}>📚 Card Codex</h2>
<div style={{ textAlign:"right" }}>
<div style={{ color:"#f1c40f", fontSize:20, fontWeight:700 }}>{ownedCount} / {ALL_CARDS.length}</div>
<div style={{ color:"#888", fontSize:12 }}>{percent}% Complete</div>
</div>
</div>
<div style={{ background:"rgba(155,89,182,0.1)", border:"1px solid rgba(155,89,182,0.3)", borderRadius:12, padding:16, marginBottom:20 }}>
<div style={{ display:"flex", gap:8, marginBottom:12 }}>
{["all","monster","magic","trap"].map(f => (
<button key={f} onClick={()=>setFilter(f)} style={{
background:filter===f?"#9b59b6":"transparent", border:`1px solid ${filter===f?"#9b59b6":"#444"}`,
borderRadius:6, padding:"4px 12px", color:filter===f?"#fff":"#888", fontSize:11, cursor:"pointer"
}}>{f.toUpperCase()}</button>
))}
</div>
<input value={search} onChange={e=>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%"
}} />
</div>
<div style={{ color:"#666", fontSize:13, marginBottom:12 }}>
{filtered.length} cards — 🔒 = Not collected
</div>
<div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fill,minmax(125px,1fr))", gap:14 }}>
{filtered.map((card,i) => (
<CardDisplay key={i} card={card} notOwned={!collectionKeys.has(getCardKey(card))} showHoverPreview={true} />
))}
</div>
</div>
);
}
// ============================================================
// 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 (
<div style={{ padding:30, maxWidth:1200, margin:"0 auto" }}>
<h2 style={{ color:"#f39c12", fontSize:28, marginBottom:24 }}>🏪 Card Shop</h2>
<div style={{ display:"flex", gap:8, marginBottom:24 }}>
{["packs","singles","sell"].map(t => (
<button key={t} onClick={()=>setTab(t)} style={{
background:tab===t?"#f39c12":"transparent",
border:`2px solid ${tab===t?"#f39c12":"#555"}`,
borderRadius:8, padding:"8px 20px",
color:tab===t?"#000":"#888", fontWeight:700, cursor:"pointer"
}}>{t==="packs"?"Card Packs":t==="singles"?"Single Cards":"Sell Cards"}</button>
))}
</div>
{packResult && (
<div style={{ background:"rgba(241,196,15,0.1)", border:"2px solid #f1c40f", borderRadius:12, padding:20, marginBottom:24 }}>
<div style={{ color:"#f1c40f", fontWeight:700, marginBottom:12 }}>Pack Opened! ({packResult.length} cards)</div>
<div style={{ display:"flex", gap:10, flexWrap:"wrap" }}>
{packResult.map((c,i) => <CardDisplay key={i} card={c} small showHoverPreview={true} />)}
</div>
<button onClick={()=>setPackResult(null)} style={{ marginTop:12, background:"#333", border:"none", borderRadius:6, padding:"6px 16px", color:"#fff", cursor:"pointer" }}>Close</button>
</div>
)}
{tab === "packs" && (
<div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fit,minmax(180px,1fr))", gap:16 }}>
{PACKS.map(pack => (
<div key={pack.id} style={{ background:"rgba(255,255,255,0.03)", border:`2px solid ${playerGold>=pack.cost?"#f39c12":"#333"}`, borderRadius:12, padding:16, textAlign:"center" }}>
<div style={{ fontSize:36, marginBottom:8 }}>{pack.icon}</div>
<div style={{ color:"#fff", fontWeight:700, fontSize:14, marginBottom:4 }}>{pack.name}</div>
<div style={{ color:"#888", fontSize:11, marginBottom:12 }}>{pack.desc}</div>
<button onClick={()=>openPack(pack)} disabled={playerGold<pack.cost} style={{
width:"100%", background:playerGold>=pack.cost?"#f39c12":"#333",
border:"none", borderRadius:6, padding:"8px", color:playerGold>=pack.cost?"#000":"#666",
fontWeight:700, cursor:playerGold>=pack.cost?"pointer":"not-allowed"
}}>{pack.cost}g</button>
</div>
))}
</div>
)}
{tab === "singles" && (
<div>
<div style={{ background:"rgba(241,196,15,0.1)", border:"1px solid #f39c12", borderRadius:8, padding:12, marginBottom:20, textAlign:"center" }}>
<div style={{ color:"#f1c40f", fontSize:14, fontWeight:700 }}>⏰ Refreshes in: {String(minutes).padStart(2,'0')}:{String(seconds).padStart(2,'0')}</div>
</div>
<div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fill,minmax(140px,1fr))", gap:16 }}>
{shopSingles.map((card,i) => (
<div key={i} style={{ display:"flex", flexDirection:"column", alignItems:"center", gap:8 }}>
<CardDisplay card={card} showHoverPreview={true} />
<button onClick={()=>buySingle(card)} disabled={playerGold<card.price} style={{
width:"100%", background:playerGold>=card.price?"#2ecc71":"#333",
border:"none", borderRadius:6, padding:"8px", color:playerGold>=card.price?"#fff":"#666",
fontWeight:700, cursor:playerGold>=card.price?"pointer":"not-allowed"
}}>{card.price}g</button>
</div>
))}
</div>
</div>
)}
{tab === "sell" && (
<div>
<div style={{ display:"flex", gap:8, marginBottom:16 }}>
{["not_equipped","equipped"].map(filter => (
<button key={filter} onClick={()=>setSellFilter(filter)} style={{
background:sellFilter===filter?"#9b59b6":"transparent",
border:`2px solid ${sellFilter===filter?"#9b59b6":"#555"}`,
borderRadius:8, padding:"6px 16px", fontSize:12,
color:sellFilter===filter?"#fff":"#888", fontWeight:700, cursor:"pointer"
}}>{filter==="equipped"?"In Deck":"Not in Deck"}</button>
))}
</div>
<div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fill,minmax(140px,1fr))", gap:16 }}>
{(() => {
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 (
<div key={i} style={{ display:"flex", flexDirection:"column", alignItems:"center", gap:8 }}>
<CardDisplay card={card} showHoverPreview={true} quantity={owned} inDeckCount={inDeck} />
<button onClick={()=>{if(canSell) onSell(card, sellPrice);}} disabled={!canSell} style={{
width:"100%", background:canSell?"#e74c3c":"#333",
border:"none", borderRadius:6, padding:"8px", color:canSell?"#fff":"#666",
fontWeight:700, cursor:canSell?"pointer":"not-allowed", fontSize:11
}}>{canSell?`Sell for ${sellPrice}g`:"All in Deck"}</button>
</div>
);
});
})()}
</div>
</div>
)}
</div>
);
}
// ============================================================
// ELEMENTS SCREEN
// ============================================================
function ElementsScreen() {
return (
<div style={{ padding:30, maxWidth:900, margin:"0 auto" }}>
<h2 style={{ color:"#2ecc71", fontSize:24, marginBottom:24 }}>⚡ Element System</h2>
<div style={{ background:"rgba(255,255,255,0.03)", border:"1px solid rgba(255,255,255,0.08)", borderRadius:12, padding:20, marginBottom:24 }}>
<p style={{ color:"#aaa", fontSize:14, lineHeight:1.8, margin:0 }}>
Element advantage grants <strong style={{ color:"#4caf50" }}>+15% damage</strong>.
Disadvantage applies <strong style={{ color:"#e74c3c" }}>-15% damage</strong>.
All card stats reset to normal after battle ends.
</p>
</div>
<div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fit,minmax(200px,1fr))", gap:16 }}>
{Object.entries(ELEMENT_CYCLE).map(([el,data]) => (
<div key={el} style={{ background:data.color+"11", border:`1px solid ${data.color}44`, borderRadius:10, padding:16, textAlign:"center" }}>
<div style={{ fontSize:40, marginBottom:8 }}>{data.icon}</div>
<div style={{ color:data.color, fontWeight:700, textTransform:"uppercase", fontSize:16, marginBottom:10 }}>{el}</div>
<div style={{ fontSize:13, color:"#aaa" }}>
<div style={{ color:"#4caf50", marginBottom:4 }}>✓ Beats {data.beats}</div>
<div style={{ color:"#e74c3c" }}>✗ Weak to {data.weakTo}</div>
</div>
</div>
))}
</div>
</div>
);
}
// ============================================================
// 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 (
<div style={{ padding: 30, maxWidth: 1400, margin: "0 auto" }}>
<h2 style={{ color: "#f39c12", fontSize: 28, marginBottom: 8 }}>🏆 {activeTournamentData.tournament.name}</h2>
<div style={{ color: "#888", marginBottom: 20 }}>Round {activeTournamentData.currentRound} / {Math.log2(activeTournamentData.tournament.participants)}</div>
<div style={{ display: "flex", gap: 20, marginBottom: 30 }}>
<div style={{ background: "rgba(241,196,15,0.1)", border: "2px solid #f1c40f", borderRadius: 12, padding: 16, flex: 1 }}>
<div style={{ color: "#888", fontSize: 11, marginBottom: 4 }}>PRIZE GOLD</div>
<div style={{ color: "#f1c40f", fontSize: 24, fontWeight: 700 }}>💰 {activeTournamentData.tournament.prizeGold}g</div>
</div>
{activeTournamentData.tournament.prizeCards > 0 && (
<div style={{ background: "rgba(155,89,182,0.1)", border: "2px solid #9b59b6", borderRadius: 12, padding: 16, flex: 1 }}>
<div style={{ color: "#888", fontSize: 11, marginBottom: 4 }}>PRIZE CARDS</div>
<div style={{ color: "#9b59b6", fontSize: 24, fontWeight: 700 }}>🎴 {activeTournamentData.tournament.prizeCards} cards</div>
</div>
)}
</div>
{/* Tournament Bracket */}
<div style={{ background: "rgba(0,0,0,0.3)", border: "1px solid rgba(255,255,255,0.1)", borderRadius: 12, padding: 20, marginBottom: 20 }}>
<h3 style={{ color: "#9b59b6", marginBottom: 16 }}>Full Tournament Bracket</h3>
<div style={{ overflowX: "auto", paddingBottom: 16 }}>
<div style={{ display: "flex", gap: 40, minWidth: "max-content", paddingBottom: 20 }}>
{(() => {
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) => (
<div key={roundIdx} style={{ display: "flex", flexDirection: "column", gap: 16, justifyContent: "space-around", minWidth: 180 }}>
<div style={{ color: "#f39c12", fontWeight: 700, fontSize: 14, textAlign: "center", marginBottom: 8 }}>
{roundInfo.round === totalRounds ? "FINAL" : roundInfo.round === totalRounds - 1 ? "SEMI-FINAL" : `ROUND ${roundInfo.round}`}
</div>
{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 (
<div key={matchIdx} style={{
background: isActive ? "rgba(241,196,15,0.2)" : isCompleted ? "rgba(76,175,80,0.1)" : "rgba(255,255,255,0.05)",
border: `2px solid ${isActive ? "#f39c12" : isCompleted ? "#4caf50" : "#444"}`,
borderRadius: 8,
padding: 12,
minHeight: 80,
display: "flex",
flexDirection: "column",
justifyContent: "center",
transition: "all 0.3s ease"
}}>
{match ? (
<>
<div style={{ color: match.player1 === "YOU" ? "#f1c40f" : "#fff", fontSize: 11, marginBottom: 4, fontWeight: match.player1 === "YOU" ? 700 : 400 }}>
{match.player1 === "YOU" ? "👤 YOU" : match.player1?.substring(0, 15) || "TBD"}
</div>
<div style={{ color: match.player2 === "YOU" ? "#f1c40f" : "#fff", fontSize: 11, fontWeight: match.player2 === "YOU" ? 700 : 400 }}>
{match.player2 === "YOU" ? "👤 YOU" : match.player2?.substring(0, 15) || "TBD"}
</div>
{isCompleted && (
<div style={{ color: "#4caf50", fontSize: 10, marginTop: 6, fontWeight: 700 }}>
Winner: {match.winner === "YOU" ? "YOU" : match.winner?.substring(0, 12)}
</div>
)}
</>
) : (
<div style={{ color: "#666", fontSize: 11, textAlign: "center" }}>TBD</div>
)}
</div>
);
})}
</div>
));
})()}
</div>
</div>
<h3 style={{ color: "#9b59b6", marginBottom: 16, marginTop: 24 }}>Current Round Details - Round {activeTournamentData.currentRound}</h3>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{bracket.map((match, i) => {
const isCurrentMatch = currentMatch === i;
const isCompleted = match.winner !== null;
const isInProgress = match.inProgress === true;
return (
<div key={i} className={isCurrentMatch && !isInProgress ? "glow" : ""} style={{
background: isInProgress ? "rgba(231,76,60,0.15)" : isCurrentMatch ? "rgba(241,196,15,0.1)" : isCompleted ? "rgba(0,255,0,0.05)" : "rgba(255,255,255,0.03)",
border: `2px solid ${isInProgress ? "#e74c3c" : isCurrentMatch ? "#f39c12" : isCompleted ? "#4caf50" : "#333"}`,
borderRadius: 8,
padding: 16,
transition: "all 0.3s ease",
position: "relative"
}}>
{i === currentMatch && !isCompleted && !isInProgress && (
<div style={{ position: "absolute", top: -10, right: -10, background: "#f39c12", color: "#000", padding: "4px 12px", borderRadius: 6, fontSize: 11, fontWeight: 900, boxShadow: "0 2px 8px rgba(0,0,0,0.5)" }}>*NEW*</div>
)}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ flex: 1 }}>
<div style={{ color: match.player1 === "YOU" ? "#f1c40f" : "#fff", fontWeight: match.player1 === "YOU" ? 700 : 400, marginBottom: 4 }}>
{match.player1 === "YOU" ? "👤 YOU" : `🎴 ${match.player1}`}
</div>
<div style={{ color: match.player2 === "YOU" ? "#f1c40f" : "#fff", fontWeight: match.player2 === "YOU" ? 700 : 400 }}>
{match.player2 === "YOU" ? "👤 YOU" : `🎴 ${match.player2}`}
</div>
</div>
<div style={{ textAlign: "right" }}>
{isCompleted ? (
<div style={{ color: "#4caf50", fontWeight: 700 }}>
✓ {match.winner === "YOU" ? "YOU WON" : `${match.winner} WON`}
</div>
) : isInProgress ? (
<div style={{ color: "#e74c3c", fontWeight: 700, animation: "pulse 2s infinite" }}>
⚔️ IN PROGRESS
</div>
) : isCurrentMatch ? (
<button onClick={() => onStartMatch(match, i)} className="bounce" style={{
background: "#e74c3c",
border: "none",
borderRadius: 8,
padding: "12px 24px",
color: "#fff",
fontWeight: 700,
fontSize: 14,
cursor: "pointer"
}}>⚔️ START MATCH</button>
) : (
<div style={{ color: "#666" }}>Waiting...</div>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
}
return (
<div style={{ padding: 30, maxWidth: 1400, margin: "0 auto" }}>
<h2 style={{ color: "#f39c12", fontSize: 32, marginBottom: 8 }}>🏆 Tournaments</h2>
<p style={{ color: "#888", marginBottom: 24 }}>Choose a tournament and test your skills against unique duelists!</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))", gap: 16 }}>
{TOURNAMENTS.map(tournament => {
const canAfford = playerGold >= tournament.entryFee;
const diffColor = difficultyColors[tournament.difficulty];
return (
<div key={tournament.id} className="fade-in" style={{
background: selectedTournament?.id === tournament.id ? "rgba(155,89,182,0.15)" : "rgba(255,255,255,0.03)",
border: `2px solid ${selectedTournament?.id === tournament.id ? "#9b59b6" : canAfford ? "#444" : "#222"}`,
borderRadius: 12,
padding: 20,
cursor: canAfford ? "pointer" : "not-allowed",
opacity: canAfford ? 1 : 0.5,
transition: "all 0.3s ease"
}} onClick={() => canAfford && setSelectedTournament(tournament)}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "start", marginBottom: 12 }}>
<h3 style={{ color: "#fff", fontSize: 18, margin: 0 }}>{tournament.name}</h3>
<div style={{
background: diffColor + "33",
border: `1px solid ${diffColor}`,
borderRadius: 6,
padding: "4px 10px",
fontSize: 10,
color: diffColor,
fontWeight: 700
}}>{tournament.difficulty}</div>
</div>
<div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
<div style={{ flex: 1 }}>
<div style={{ color: "#888", fontSize: 10, marginBottom: 4 }}>ENTRY FEE</div>
<div style={{ color: tournament.entryFee === 0 ? "#4caf50" : "#f39c12", fontSize: 16, fontWeight: 700 }}>
{tournament.entryFee === 0 ? "FREE" : `${tournament.entryFee}g`}
</div>
</div>
<div style={{ flex: 1 }}>
<div style={{ color: "#888", fontSize: 10, marginBottom: 4 }}>PARTICIPANTS</div>
<div style={{ color: "#fff", fontSize: 16, fontWeight: 700 }}>{tournament.participants}</div>
</div>
</div>
<div style={{ background: "rgba(0,0,0,0.3)", borderRadius: 8, padding: 12, marginBottom: 12 }}>
<div style={{ color: "#888", fontSize: 10, marginBottom: 8 }}>PRIZES</div>
{tournament.prizeGold > 0 && (
<div style={{ color: "#f1c40f", fontSize: 14, marginBottom: 4 }}>💰 {tournament.prizeGold} gold</div>
)}
{tournament.prizeCards > 0 && (
<div style={{ color: "#9b59b6", fontSize: 14 }}>🎴 {tournament.prizeCards} random cards</div>
)}
</div>
{!canAfford && (
<div style={{ color: "#e74c3c", fontSize: 11, textAlign: "center", marginTop: 8 }}>
❌ Need {tournament.entryFee - playerGold} more gold
</div>
)}
</div>
);
})}
</div>
{selectedTournament && (
<div style={{
position: "fixed",
bottom: 20,
left: "50%",
transform: "translateX(-50%)",
background: "#9b59b6",
border: "none",
borderRadius: 12,
padding: 20,
boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
zIndex: 1000
}} className="bounce">
<button onClick={() => onRegister(selectedTournament)} style={{
background: "#4caf50",
border: "none",
borderRadius: 8,
padding: "16px 48px",
color: "#fff",
fontSize: 18,
fontWeight: 700,
cursor: "pointer"
}}>
🎫 REGISTER FOR {selectedTournament.name.toUpperCase()}
</button>
</div>
)}
</div>
);
}
// ============================================================
// 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 (
<div style={{ padding:30, display:"flex", alignItems:"center", justifyContent:"center", height:"calc(100vh - 52px)" }}>
<div style={{ textAlign:"center", opacity:0.3 }}>
<div style={{ fontSize:80, marginBottom:16 }}>⚔️</div>
<h2 style={{ color:"#666", fontSize:24 }}>No Active Battle</h2>
<p style={{ color:"#444", fontSize:14 }}>Accept a duel in Story mode to begin!</p>
</div>
</div>
);
}
// Check win conditions
if (battleState.player.hp <= 0) {
return (
<div style={{ padding:30, textAlign:"center" }}>
<h2 style={{ color:"#e74c3c", fontSize:32 }}>💀 DEFEAT</h2>
<p style={{ color:"#aaa" }}>You lost the duel...</p>
<button onClick={() => onBattleEnd(false)} style={{ background:"#e74c3c", border:"none", borderRadius:8, padding:"12px 24px", color:"#fff", fontWeight:700, cursor:"pointer", marginTop:20 }}>Return to Story</button>
</div>
);
}
if (battleState.opponent.hp <= 0) {
return (
<div style={{ padding:30, textAlign:"center" }}>
<h2 style={{ color:"#4caf50", fontSize:32 }}>🏆 VICTORY!</h2>
<p style={{ color:"#aaa" }}>You won the duel and earned 50 gold!</p>
<button onClick={() => onBattleEnd(true)} style={{ background:"#4caf50", border:"none", borderRadius:8, padding:"12px 24px", color:"#fff", fontWeight:700, cursor:"pointer", marginTop:20 }}>Return to Story</button>
</div>
);
}
return (
<div style={{ padding:20, height:"calc(100vh - 52px)", display:"flex", flexDirection:"column", gap:12 }}>
{/* HP Bars and Counters */}
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center" }}>
<div style={{ display:"flex", gap:16, alignItems:"center" }}>
<div>
<div style={{ color:"#888", fontSize:11 }}>OPPONENT</div>
<div style={{ color:"#e74c3c", fontSize:20, fontWeight:700 }}>❤️ {battleState.opponent.hp}</div>
</div>
<div onClick={() => 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" }}>
<div style={{ color:"#9b59b6", fontSize:10 }}>GRAVEYARD</div>
<div style={{ color:"#fff", fontSize:16, fontWeight:700 }}>💀 {battleState.opponent.graveyard.length}</div>
</div>
<div style={{ textAlign:"center", background:"rgba(74,144,226,0.2)", padding:"8px 12px", borderRadius:6, border:"1px solid #4a90e2" }}>
<div style={{ color:"#4a90e2", fontSize:10 }}>DECK</div>
<div style={{ color:"#fff", fontSize:16, fontWeight:700 }}>🎴 {battleState.opponent.drawPile.length}</div>
</div>
</div>
<div style={{ color:"#f39c12", fontSize:14, fontWeight:700, textTransform:"uppercase", textAlign:"center" }}>
{battleState.turn === "player" ? "YOUR TURN" : "OPPONENT'S TURN"}
<div style={{ color:"#666", fontSize:10, marginTop:4 }}>Turn {battleState.turnCount}</div>
</div>
<div style={{ display:"flex", gap:16, alignItems:"center" }}>
<div style={{ textAlign:"center", background:"rgba(74,144,226,0.2)", padding:"8px 12px", borderRadius:6, border:"1px solid #4a90e2" }}>
<div style={{ color:"#4a90e2", fontSize:10 }}>DECK</div>
<div style={{ color:"#fff", fontSize:16, fontWeight:700 }}>🎴 {battleState.player.drawPile.length}</div>
</div>
<div onClick={() => 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" }}>
<div style={{ color:"#9b59b6", fontSize:10 }}>GRAVEYARD</div>
<div style={{ color:"#fff", fontSize:16, fontWeight:700 }}>💀 {battleState.player.graveyard.length}</div>
</div>
<div>
<div style={{ color:"#888", fontSize:11 }}>{playerName.toUpperCase()}</div>
<div style={{ color:"#4caf50", fontSize:20, fontWeight:700 }}>❤️ {battleState.player.hp}</div>
</div>
</div>
</div>
{/* Opponent Monster Field */}
<div style={{ background:"rgba(231,76,60,0.1)", border:"1px solid #e74c3c", borderRadius:8, padding:12, minHeight:100 }}>
<div style={{ color:"#e74c3c", fontSize:11, marginBottom:8 }}>OPPONENT MONSTERS ({battleState.opponent.monsterField.length}/5)</div>
<div style={{ display:"flex", gap:8, flexWrap:"wrap" }}>
{battleState.opponent.monsterField.map((card, i) => (
<div key={i} onClick={() => battleState.turn === "player" && setSelectedTarget(i)}
style={{ cursor: battleState.turn === "player" ? "pointer" : "default", opacity: selectedTarget === i ? 1 : 0.8 }}>
<CardDisplay card={card} small selected={selectedTarget === i} />
<div style={{ textAlign:"center", color:"#2ecc71", fontSize:9, marginTop:2 }}>{card.currentHp} HP</div>
</div>
))}
{battleState.turn === "player" && battleState.opponent.monsterField.length === 0 && (
<div onClick={() => 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" }}>
<span style={{ color:"#888", fontSize:11 }}>DIRECT ATTACK</span>
</div>
)}
</div>
</div>
{/* Opponent Spell/Trap Field */}
<div style={{ background:"rgba(74,144,226,0.05)", border:"1px solid rgba(74,144,226,0.3)", borderRadius:8, padding:8, minHeight:60 }}>
<div style={{ color:"#4a90e2", fontSize:10, marginBottom:6 }}>OPPONENT SPELL/TRAP ({battleState.opponent.spellTrapField.length}/5)</div>
<div style={{ display:"flex", gap:6, flexWrap:"wrap" }}>
{battleState.opponent.spellTrapField.map((card, i) => (
<div key={i} style={{ width:60, height:84, background:"#1a1a1a", border:"2px solid #4a90e2", borderRadius:6, display:"flex", alignItems:"center", justifyContent:"center", fontSize:24 }}>
{card.faceDown ? "🂠" : card.type === "magic" ? "✨" : "⚡"}
</div>
))}
</div>
</div>
{/* Battle Log */}
<div ref={battleLogRef} style={{ background:"rgba(0,0,0,0.3)", border:"1px solid rgba(255,255,255,0.1)", borderRadius:8, padding:8, height:80, overflowY:"auto" }}>
{battleLog.slice(-5).map(log => (
<div key={log.id} className="slide-in" style={{ color:"#aaa", fontSize:11, marginBottom:4 }}>{log.text}</div>
))}
</div>
{/* Player Spell/Trap Field */}
<div style={{ background:"rgba(74,144,226,0.05)", border:"1px solid rgba(74,144,226,0.3)", borderRadius:8, padding:8, minHeight:60 }}>
<div style={{ color:"#4a90e2", fontSize:10, marginBottom:6 }}>YOUR SPELL/TRAP ({battleState.player.spellTrapField.length}/5)</div>
<div style={{ display:"flex", gap:6, flexWrap:"wrap" }}>
{battleState.player.spellTrapField.map((card, i) => (
<div key={i} style={{ width:60, height:84, background:"#1a1a1a", border:"2px solid #4a90e2", borderRadius:6, display:"flex", flexDirection:"column", alignItems:"center", justifyContent:"center", fontSize:card.faceDown?24:10, color:"#4a90e2", padding:4 }}>
{card.faceDown ? "🂠" : (
<>
<span style={{ fontSize:20, marginBottom:4 }}>{card.type === "magic" ? "✨" : "⚡"}</span>
<span style={{ fontSize:8, textAlign:"center" }}>{card.name.substring(0,8)}</span>
</>
)}
</div>
))}
</div>
</div>
{/* Player Monster Field */}
<div style={{ background:"rgba(76,175,80,0.1)", border:"1px solid #4caf50", borderRadius:8, padding:12, minHeight:100 }}>
<div style={{ color:"#4caf50", fontSize:11, marginBottom:8 }}>YOUR MONSTERS ({battleState.player.monsterField.length}/5)</div>
<div style={{ display:"flex", gap:8, flexWrap:"wrap" }}>
{battleState.player.monsterField.map((card, i) => (
<div key={i}>
<CardDisplay card={card} small />
<div style={{ textAlign:"center", color:"#2ecc71", fontSize:9, marginTop:2 }}>{card.currentHp} HP</div>
{battleState.turn === "player" && card.canAttack && battleState.turnCount > 1 && (
<button onClick={() => attack(i)} style={{ width:"100%", background:"#e74c3c", border:"none", borderRadius:4, padding:"4px", color:"#fff", fontSize:9, marginTop:4, cursor:"pointer" }}>ATTACK</button>
)}
{battleState.turn === "player" && battleState.turnCount === 1 && (
<div style={{ width:"100%", background:"#666", border:"none", borderRadius:4, padding:"4px", color:"#333", fontSize:8, marginTop:4, textAlign:"center" }}>Turn 1</div>
)}
</div>
))}
</div>
</div>
{/* Player Hand */}
<div style={{ background:"rgba(0,0,0,0.5)", border:"1px solid rgba(255,255,255,0.2)", borderRadius:8, padding:12 }}>
<div style={{ display:"flex", justifyContent:"space-between", marginBottom:8 }}>
<div style={{ color:"#f39c12", fontSize:11 }}>YOUR HAND ({battleState.player.hand.length})</div>
{battleState.turn === "player" && (
<button onClick={endTurn} style={{ background:"#f39c12", border:"none", borderRadius:6, padding:"4px 12px", color:"#000", fontSize:11, fontWeight:700, cursor:"pointer" }}>END TURN</button>
)}
</div>
<div style={{ display:"flex", gap:8, flexWrap:"wrap" }}>
{battleState.player.hand.map((card, i) => (
<div key={i} onClick={() => battleState.turn === "player" && playCard(card, "player")}
style={{ cursor: battleState.turn === "player" ? "pointer" : "not-allowed" }}>
<CardDisplay card={card} small selected={selectedCard === card} showHoverPreview={true} />
</div>
))}
</div>
</div>
{/* Graveyard Viewer Modal */}
{viewingGraveyard && (
<div style={{ position:"fixed", inset:0, background:"rgba(0,0,0,0.9)", zIndex:1000, display:"flex", alignItems:"center", justifyContent:"center", padding:20 }} onClick={() => setViewingGraveyard(null)}>
<div onClick={(e) => e.stopPropagation()} style={{ background:"#1a1a1a", border:"2px solid #9b59b6", borderRadius:12, padding:24, maxWidth:800, maxHeight:"80vh", overflowY:"auto" }}>
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:16 }}>
<h3 style={{ color:"#9b59b6", margin:0 }}>
💀 {viewingGraveyard.side === "player" ? "Your" : "Opponent's"} Graveyard ({viewingGraveyard.cards.length} cards)
</h3>
<button onClick={() => setViewingGraveyard(null)} style={{ background:"#e74c3c", border:"none", borderRadius:6, padding:"8px 16px", color:"#fff", fontWeight:700, cursor:"pointer" }}>✕ Close</button>
</div>
{viewingGraveyard.cards.length === 0 ? (
<div style={{ color:"#666", textAlign:"center", padding:40 }}>No cards in graveyard</div>
) : (
<div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fill, minmax(90px, 1fr))", gap:12 }}>
{viewingGraveyard.cards.map((card, i) => (
<CardDisplay key={i} card={card} small showHoverPreview={true} />
))}
</div>
)}
</div>
</div>
)}
</div>
);
}
// ============================================================
// 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 (
<div style={{ minHeight:"100vh", background:"#07070f", display:"flex",alignItems:"center",justifyContent:"center", fontFamily:"'Lato',sans-serif" }}>
<style>{`@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@900&family=Lato:wght@400;700&display=swap');`}</style>
<div style={{ textAlign:"center", maxWidth:500, padding:40 }}>
<div style={{ fontSize:80, marginBottom:12 }}>🌙</div>
<h1 style={{ fontSize:52,fontWeight:900,color:"#9b59b6",letterSpacing:6,fontFamily:"'Cinzel',serif" }}>DUEL MASTERS</h1>
<p style={{ color:"#555",fontSize:13,letterSpacing:4,marginBottom:48 }}>1000 CARDS • 8 ELEMENTS</p>
<div style={{ background:"rgba(155,89,182,0.08)", border:"1px solid rgba(155,89,182,0.2)", borderRadius:12,padding:30 }}>
<p style={{ color:"#888",marginBottom:16 }}>Enter your name, Duelist</p>
<input value={nameInput} onChange={e=>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"
}} />
<button onClick={startGame} style={{
background:"#9b59b6", border:"none", borderRadius:10, padding:"14px",
color:"#fff", fontSize:16, fontWeight:700, cursor:"pointer", width:"100%"
}}>BEGIN DUEL →</button>
</div>
</div>
</div>
);
}
return (
<div style={{ minHeight:"100vh", background:"#07070f", color:"#e0e0e0", fontFamily:"'Lato',sans-serif" }}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@700&family=Lato:wght@400;700&display=swap');
* { box-sizing:border-box; }
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes glow {
0%, 100% { box-shadow: 0 0 5px rgba(155,89,182,0.5); }
50% { box-shadow: 0 0 20px rgba(155,89,182,0.8), 0 0 30px rgba(155,89,182,0.4); }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
@keyframes cardFlip {
0% { transform: rotateY(0deg); }
100% { transform: rotateY(180deg); }
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.fade-in { animation: fadeIn 0.5s ease-out; }
.slide-in { animation: slideIn 0.4s ease-out; }
.glow { animation: glow 2s ease-in-out infinite; }
.shake { animation: shake 0.5s ease-in-out; }
.bounce { animation: bounce 1s ease-in-out infinite; }
button { transition: all 0.3s ease; }
button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
button:active:not(:disabled) { transform: translateY(0); }
`}</style>
<div style={{ background:"rgba(0,0,0,0.8)", borderBottom:"1px solid rgba(155,89,182,0.2)", padding:"0 20px", display:"flex", justifyContent:"space-between", alignItems:"center", position:"sticky", top:0, zIndex:50, height:52 }}>
<div style={{ display:"flex",alignItems:"center",gap:16 }}>
<span style={{ color:"#9b59b6", fontWeight:900, fontSize:16, fontFamily:"'Cinzel',serif" }}>🌙 DUEL MASTERS</span>
<div style={{ display:"flex", gap:2 }}>
{TABS.map(tab => (
<button key={tab.id} onClick={()=>!tab.disabled && setActiveTab(tab.id)} className={activeTab===tab.id?"glow":""} style={{
background:activeTab===tab.id?"rgba(155,89,182,0.2)":"transparent",
border:"none", borderBottom:activeTab===tab.id?"2px solid #9b59b6":"2px solid transparent",
padding:"14px 10px", color:tab.disabled?"#333":activeTab===tab.id?"#9b59b6":"#666",
fontWeight:700, fontSize:10, cursor:tab.disabled?"not-allowed":"pointer",
opacity:tab.disabled?0.4:1, transition:"all 0.3s ease"
}}>
{tab.icon} {tab.label}
</button>
))}
</div>
</div>
<div style={{ display:"flex", gap:16, alignItems:"center" }}>
<span style={{ color:"#f1c40f", fontWeight:700, fontSize:16 }}>💰 {playerGold}g</span>
<span style={{ color:"#9b59b6", fontSize:14 }}>👤 {playerName}</span>
</div>
</div>
<div style={{ minHeight:"calc(100vh - 52px)" }}>
{activeTab === "deck" && <DeckScreen playerDeck={playerDeck} playerCollection={playerCollection} onSave={setPlayerDeck} />}
{activeTab === "collection" && <CollectionScreen collection={playerCollection} />}
{activeTab === "codex" && <CodexScreen playerCollection={playerCollection} />}
{activeTab === "shop" && <ShopScreen playerGold={playerGold} playerCollection={playerCollection} playerDeck={playerDeck} onBuy={handleShopBuy} onSell={handleSell} />}
{activeTab === "elements" && <ElementsScreen />}
{activeTab === "tournament" && <TournamentScreen playerGold={playerGold} onRegister={handleTournamentRegister} activeTournamentData={activeTournamentData} onStartMatch={handleStartMatch} />}
{activeTab === "battle" && <BattleScreen inBattle={inBattle} playerDeck={playerDeck} playerName={playerName} onBattleEnd={handleBattleEnd} currentBattleInfo={currentBattleInfo} />}
</div>
</div>
);
}Game Source: DUEL MASTERS
Creator: FrostCobra30
Libraries: none
Complexity: complex (2154 lines, 105.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: duel-masters-frostcobra30" to link back to the original. Then publish at arcadelab.ai/publish.