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.type==="monster"?"š":card.type==="magic"?"āØ":"ā”"}
{card.type==="monster" && (
{[["ATK",card.atk],["DEF",card.def],["HP",card.hp]].map(([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]) => (
))}
)}
{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" && }
);
}