// ============================================================= // BATTLE SYSTEM — turn-based vs NPC trainers // 1. Trainer list // 2. Team selection from your collection // 3. Battle screen with attacks, switching, log // 4. Victory / defeat // ============================================================= const { useState: useBatState, useEffect: useBatEffect, useRef: useBatRef, useMemo: useBatMemo } = React; function Battles({ state, setState, onBack }) { // sub-phase: list | team | fight | result const [phase, setPhase] = useBatState("list"); const [trainer, setTrainer] = useBatState(null); const [team, setTeam] = useBatState([]); // player team (pokemon objects) const [result, setResult] = useBatState(null); // {win, trainer, log} function selectTrainer(t) { setTrainer(t); setPhase("team"); } function startBattle(playerTeam) { setTeam(playerTeam); setPhase("fight"); } function finishBattle(winInfo) { setResult(winInfo); if (winInfo.win) { setState(prev => { const next = { ...prev }; if (!next.badges.includes(trainer.id)) next.badges = [...next.badges, trainer.id]; next.battlesWon = (next.battlesWon || 0) + 1; next.coins = (next.coins || 0) + trainer.reward * 10; return next; }); } setPhase("result"); } if (phase === "list") return ; if (phase === "team") return setPhase("list")}/>; if (phase === "fight") return ; if (phase === "result") return setPhase("list")} onHub={onBack}/>; return null; } // ============================================================= // TRAINER LIST // ============================================================= function TrainerList({ state, onPick, onBack }) { return (
ARENA NPC
SELECCIONA ENTRENADOR
MEDALLAS: {state.badges.length}/{TRAINERS.length}
{TRAINERS.map(t => { const earned = state.badges.includes(t.id); return (
onPick(t)} style={{ background: "linear-gradient(135deg, #161B22 0%, #0D1117 100%)", border: `1px solid ${earned ? "var(--accent-orange)" : "var(--border-hairline)"}`, borderRadius: 4, padding: 20, cursor:"pointer", position:"relative", boxShadow: earned ? "0 0 16px var(--accent-orange-glow)" : "var(--elev-offset-1)", transition:"all 200ms", }} onMouseEnter={e => e.currentTarget.style.transform="translateY(-2px)"} onMouseLeave={e => e.currentTarget.style.transform="none"} > {earned && (
)}
{t.avatar}
{t.badge}
{t.name}
{t.title}
"{t.quote}"
DIFICULTAD
{Array.from({length:5}).map((_,i)=>(
))}
{t.deck.map(id => { const p = DEX_BY_ID[id]; return p ? (
) : null; })}
RECOMP.
+{t.reward * 10}c
); })}
); } // ============================================================= // TEAM SELECT // ============================================================= function TeamSelect({ state, trainer, onStart, onBack }) { const owned = useBatMemo(() => { let list = Object.keys(state.collection).map(id => DEX_BY_ID[+id]).filter(Boolean); // If no collection, grant starter team if (list.length === 0) { list = [DEX_BY_ID[4], DEX_BY_ID[7], DEX_BY_ID[1], DEX_BY_ID[25]]; } return list.sort((a,b) => b.hp - a.hp); }, [state.collection]); const [selected, setSelected] = useBatState([]); const max = 4; function toggle(p) { if (selected.find(x => x.id === p.id)) setSelected(selected.filter(x => x.id !== p.id)); else if (selected.length < max) setSelected([...selected, p]); } return (
BATALLA VS {trainer.name.toUpperCase()}
SELECCIONA TU EQUIPO (1–4)
{selected.length}/{max} SELECCIONADOS
TU COLECCIÓN · {owned.length} POKÉMON DISPONIBLES
{owned.map(p => { const isSel = selected.find(x => x.id === p.id); return (
{isSel && (
{selected.indexOf(isSel) + 1}
)} toggle(p)} glow={!!isSel} />
); })}
{/* Side panel */}
RIVAL
{trainer.avatar}
{trainer.name}
{trainer.title}
SU EQUIPO
{trainer.deck.map(id => { const p = DEX_BY_ID[id]; return p ? (
{p.name}
{p.hp} PS · {RARITY[p.rarity].name}
) : null; })}
TU EQUIPO
{selected.length === 0 && (
▸ ELIGE 1–4 POKÉMON
)} {selected.map((p,i) => (
{i+1}.
{p.name}
))}
{Object.keys(state.collection).length === 0 && (
ⓘ Recibes un equipo prestado de prueba si aún no tienes colección
)}
); } // ============================================================= // BATTLE SCREEN — actual turn-based combat // ============================================================= function BattleScreen({ trainer, playerTeam, trainerName, onFinish }) { // Each combatant: {p: pokemon, hp: currentHP, idx} const mkUnit = (p, idx) => ({ p, hp: p.hp, idx }); const [player, setPlayer] = useBatState(() => playerTeam.map(mkUnit)); const [enemy, setEnemy] = useBatState(() => trainer.deck.map(id => DEX_BY_ID[id]).filter(Boolean).map(mkUnit)); const [activeP, setActiveP] = useBatState(0); const [activeE, setActiveE] = useBatState(0); const [turn, setTurn] = useBatState("player"); // player | enemy const [turnNum, setTurnNum] = useBatState(1); const [log, setLog] = useBatState([ { kind:"system", text:`¡${trainer.name} reta a un combate!` }, { kind:"system", text:`Va con ${DEX_BY_ID[trainer.deck[0]]?.name.toUpperCase()}.` }, ]); const [busy, setBusy] = useBatState(false); const [attackFlash, setAttackFlash] = useBatState(null); // "player" | "enemy" | null const [hitFlash, setHitFlash] = useBatState(null); const [shake, setShake] = useBatState(null); const playerUnit = player[activeP]; const enemyUnit = enemy[activeE]; function pushLog(entry) { setLog(l => [...l, entry]); } // ---- effects ---- function performAttack(attacker, defender, attack, side) { return new Promise(resolve => { setBusy(true); setAttackFlash(side); const dmg = GameEngine.applyEffectiveness(attacker.p.types[0], defender.p, attack.dmg); const isWeak = dmg > attack.dmg; pushLog({ kind: side, text: `${attacker.p.name.toUpperCase()} usa ${attack.name}!` }); setTimeout(() => { setAttackFlash(null); setHitFlash(side === "player" ? "enemy" : "player"); setShake(side === "player" ? "enemy" : "player"); if (attack.dmg > 0) { pushLog({ kind:"dmg", text: `${defender.p.name} recibe ${dmg} de daño${isWeak ? " (¡SUPER EFECTIVO! ×2)" : ""}.` }); if (side === "player") { setEnemy(prev => prev.map((u,i) => i === activeE ? {...u, hp: Math.max(0, u.hp - dmg)} : u)); } else { setPlayer(prev => prev.map((u,i) => i === activeP ? {...u, hp: Math.max(0, u.hp - dmg)} : u)); } } else if (attack.note) { if (attack.note.includes("Cura")) { const heal = parseInt(attack.note.match(/\d+/)?.[0] || "0"); if (side === "player") setPlayer(p => p.map((u,i) => i === activeP ? {...u, hp: Math.min(u.p.hp, u.hp + heal)} : u)); else setEnemy(p => p.map((u,i) => i === activeE ? {...u, hp: Math.min(u.p.hp, u.hp + heal)} : u)); pushLog({ kind:"heal", text: `${attacker.p.name} recupera ${heal} PS.` }); } else { pushLog({ kind:"effect", text: attack.note }); } } setTimeout(() => { setHitFlash(null); setShake(null); resolve(); }, 380); }, 520); }); } async function playerAttack(attack) { if (busy || turn !== "player") return; await performAttack(playerUnit, enemyUnit, attack, "player"); // Check KO const newEnemyHP = Math.max(0, enemyUnit.hp - GameEngine.applyEffectiveness(playerUnit.p.types[0], enemyUnit.p, attack.dmg)); if (newEnemyHP <= 0) { pushLog({ kind:"ko", text:`¡${enemyUnit.p.name.toUpperCase()} fue debilitado!` }); await sleep(700); // Find next enemy const nextE = enemy.findIndex((u, i) => i !== activeE && (i === activeE ? false : true) && (function(){ // need updated hp // we use enemy state directly; but state may lag. Use predicted. return u.hp > 0; })()); // Compute fresh next using post-damage values const updated = enemy.map((u,i)=> i===activeE ? {...u, hp:0} : u); const next = updated.findIndex(u => u.hp > 0); if (next === -1) { onFinish({ win:true, log }); return; } setActiveE(next); pushLog({ kind:"system", text:`${trainer.name} envía a ${updated[next].p.name.toUpperCase()}.` }); setBusy(false); setTurn("enemy"); setTimeout(enemyTurn, 800); return; } setBusy(false); setTurn("enemy"); setTimeout(enemyTurn, 700); } function playerSwitch(toIdx) { if (busy || turn !== "player") return; if (toIdx === activeP || player[toIdx].hp <= 0) return; pushLog({ kind:"system", text:`¡Vuelve, ${playerUnit.p.name.toUpperCase()}! ¡Adelante ${player[toIdx].p.name.toUpperCase()}!` }); setActiveP(toIdx); setTurn("enemy"); setTimeout(enemyTurn, 700); } async function enemyTurn() { const e = enemy[activeE]; if (!e || e.hp <= 0) { setBusy(false); setTurn("player"); return; } const atk = GameEngine.aiPickAttack(e.p, turnNum); await performAttack(e, player[activeP], atk, "enemy"); // Check player KO const dmg = GameEngine.applyEffectiveness(e.p.types[0], player[activeP].p, atk.dmg); const newHP = Math.max(0, player[activeP].hp - dmg); if (newHP <= 0) { pushLog({ kind:"ko", text:`¡Tu ${player[activeP].p.name.toUpperCase()} fue debilitado!` }); await sleep(700); const updated = player.map((u,i)=> i===activeP ? {...u, hp:0} : u); const next = updated.findIndex(u => u.hp > 0); if (next === -1) { onFinish({ win:false, log }); return; } setActiveP(next); pushLog({ kind:"system", text:`¡Adelante, ${updated[next].p.name.toUpperCase()}!` }); } setTurnNum(t => t + 1); setBusy(false); setTurn("player"); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } const playerInfo = TYPE_INFO[playerUnit.p.types[0]]; const enemyInfo = TYPE_INFO[enemyUnit.p.types[0]]; return (
{/* Top bar */}
COMBATE · TURNO {turnNum} · {turn === "player" ? TU TURNO : RIVAL ACTÚA} VS {trainer.name.toUpperCase()}
{/* Enemy side */}
{/* Enemy bench */}
BANCA RIVAL
{enemy.map((u,i) => i === activeE ? null : ( ))}
{/* Enemy active */}
{/* Center log */}
{[...log].reverse().slice(0, 30).map((e, i) => { const color = e.kind === "player" ? "var(--accent-cyan)" : e.kind === "enemy" ? "var(--accent-orange)" : e.kind === "dmg" ? "var(--accent-yellow)" : e.kind === "ko" ? "var(--accent-red)" : e.kind === "heal" ? "var(--accent-lime)" : "var(--fg-3)"; const prefix = e.kind === "player" ? "▸ TÚ" : e.kind === "enemy" ? "◂ RIVAL" : e.kind === "dmg" ? "✦ DMG" : e.kind === "ko" ? "✕ KO" : e.kind === "heal" ? "+ HEAL" : "· LOG"; return (
{prefix}{e.text}
); })}
{/* Player side */}
TU BANCA · click para cambiar
{player.map((u,i) => i === activeP ? null : ( 0} onClick={() => playerSwitch(i)}/> ))}
{/* Action bar */}
{playerUnit.p.attacks.map((atk, i) => { const projected = GameEngine.applyEffectiveness(playerUnit.p.types[0], enemyUnit.p, atk.dmg); const isWeak = projected > atk.dmg; return ( ); })}
); } // Active combatant card with HP bar + image function ActiveCombatant({ unit, side, isAttacking, isHit, shake, trainerName, trainerAvatar }) { const info = TYPE_INFO[unit.p.types[0]]; const hpPct = (unit.hp / unit.p.hp) * 100; const hpColor = hpPct > 50 ? "var(--accent-lime)" : hpPct > 20 ? "var(--accent-yellow)" : "var(--accent-red)"; return (
{/* Mini trainer + nameplate */}
{trainerAvatar}
{trainerName.toUpperCase()}
{unit.p.name}
#{String(unit.p.id).padStart(3,"0")}
{/* HP bar */}
PS
{/* HP segments */} {Array.from({length: Math.floor(unit.p.hp/30)}).map((_,i) => (
))}
{unit.hp}/{unit.p.hp}
{/* Big sprite */}
{/* Platform */}
{unit.p.name}
); } function BenchSlot({ unit, onClick, clickable }) { const info = TYPE_INFO[unit.p.types[0]]; const hpPct = (unit.hp / unit.p.hp) * 100; const dead = unit.hp <= 0; return (
50 ? "var(--accent-lime)" : hpPct > 20 ? "var(--accent-yellow)" : "var(--accent-red)"}}/>
{dead &&
}
); } // ============================================================= // BATTLE RESULT // ============================================================= function BattleResult({ result, trainer, onBack, onHub }) { const win = result.win; return (
─── RESULTADO DEL COMBATE ───

{win ? "VICTORIA" : "DERROTA"}

{trainer.avatar}
{trainer.name}
{trainer.title}
{win ? (
{trainer.avatar}
MEDALLA OBTENIDA
{trainer.badge}
+{trainer.reward * 10} Pokécoins
) : (
La derrota es maestra. Vuelve cuando tengas mejores cartas. Abre más sobres y prueba otra vez.
)}
); } window.Battles = Battles;