diff --git a/public/background_register.png b/public/background_register.png new file mode 100644 index 0000000..115be3d Binary files /dev/null and b/public/background_register.png differ diff --git a/public/label.png b/public/label.png new file mode 100644 index 0000000..a319910 Binary files /dev/null and b/public/label.png differ diff --git a/public/pc_container_bg.png b/public/pc_container_bg.png new file mode 100644 index 0000000..3602268 Binary files /dev/null and b/public/pc_container_bg.png differ diff --git a/public/selection_layout.png b/public/selection_layout.png new file mode 100644 index 0000000..ab09bff Binary files /dev/null and b/public/selection_layout.png differ diff --git a/src/App.css b/src/App.css index bd0432c..3c21e70 100644 --- a/src/App.css +++ b/src/App.css @@ -1,19 +1,13 @@ -.App { - text-align: center; - - background-image: url('resources/background.jpg'); - background-size: cover; +body { + background-color: white; + color: black; + margin: 0; + font-family: sans-serif; } - -.App-header { - - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - - +.app-layout { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 93ae8bc..4c0d569 100644 --- a/src/App.js +++ b/src/App.js @@ -1,33 +1,78 @@ +import React, { useState } from 'react'; import './App.css'; -import PokemonCard from './components/PokemonCard'; +import { GAME_PHASES } from './utils/constants'; + +// --- IMPORTS DE PANTALLAS --- +import RegisterScreen from './screens/register/RegisterScreen'; +import SelectionScreen from './screens/selection/SelectionScreen'; +import PageLabel from './components/PageLabel'; +import BattleScreen from './screens/battle/BattleScreen'; function App() { - return ( -
-
- - { - - } - { - - } - -
-
- ); + const [phase, setPhase] = useState(GAME_PHASES.REGISTER); + const [players, setPlayers] = useState(null); + const [teams, setTeams] = useState(null); + + const handleRegisterComplete = (playerData) => { + setPlayers(playerData); + setPhase(GAME_PHASES.SELECTION); + }; + + const handleBattleStart = (teamData) => { + setTeams(teamData); + setPhase(GAME_PHASES.BATTLE); + }; + + // --- NUEVO: Volver a selección sin borrar jugadores --- + const handleBackToSelection = () => { + // Solo cambiamos la fase, 'players' se mantiene en memoria + setPhase(GAME_PHASES.SELECTION); + }; + + const handleReset = () => { + setPlayers(null); // Aquí sí borramos todo + setTeams(null); + setPhase(GAME_PHASES.REGISTER); + }; + + return ( +
+ +
+ {/* FASE 1: REGISTRO */} + {phase === GAME_PHASES.REGISTER && ( + + )} + + {/* FASE 2: SELECCIÓN */} + {phase === GAME_PHASES.SELECTION && ( + + )} + + {/* FASE 3: BATALLA */} + {phase === GAME_PHASES.BATTLE && ( +
+ + + {/* Pasamos la nueva función onBack */} + +
+ )} +
+
+ ); } -export default App; +export default App; \ No newline at end of file diff --git a/src/assets/label.png b/src/assets/label.png new file mode 100644 index 0000000..a319910 Binary files /dev/null and b/src/assets/label.png differ diff --git a/src/components/ActionButton.css b/src/components/ActionButton.css new file mode 100644 index 0000000..ae678a2 --- /dev/null +++ b/src/components/ActionButton.css @@ -0,0 +1,67 @@ +/*ESTILO GENERAL PARA TODOS LOS ACTION-BUTTONS QUE DECLAREMOSe*/ +.btn-action { + /*Damos espacio tanto arriba, abajo, y en los laterales */ + padding: 14px 24px; + /*Quitamos el borde*/ + border: none; + /* Forma de píldora (como Start/Select o botones A/B) */ + border-radius: 50px; + margin-top: 10px; + /* TIPOGRAFÍA */ + font-family: 'Press Start 2P', cursive, sans-serif; + font-size: 0.85rem; + font-weight: bold; + letter-spacing: 1px; + cursor: pointer; + /* EFECTO PLÁSTICO 3D (El truco Nintendo) */ + /* Sombra 1 (inset blanco): Brillo superior (luz) */ + /* Sombra 2 (inset negro): Sombra inferior (volumen) */ + /* Sombra 3 (externa): La sombra que proyecta el botón en la carcasa */ + box-shadow: + inset 0px 4px 0px rgba(255, 255, 255, 0.4), + inset 0px -4px 0px rgba(0, 0, 0, 0.2), + 0px 6px 0px rgba(0, 0, 0, 0.3); + +} + +.btn-action:disabled { + background-color: #9ea7b4; + border: none; + /* Gris plástico viejo */ + color: #6a737d; + cursor: not-allowed; + transform: translateY(4px); + /* Se queda hundido */ + /* Reutilizo la escala de sombras*/ + box-shadow: + inset 0px 4px 0px rgba(255, 255, 255, 0.4), + inset 0px -4px 0px rgba(0, 0, 0, 0.2), + 0px 6px 0px rgba(0, 0, 0, 0.3); +} + +/* --- COLORES --- */ +/* PRIMARY: El Botón "A" (Rojo Gameboy Clásico) */ +.primary { + background-color: #c6616b; + color: white; + text-shadow: 2px 2px 0px #a13d47; + /* Texto con relieve */ +} + +/* Un pequeño ajuste al hover para dar feedback */ +.primary:hover:not(:disabled) { + background-color: #a13d47; +} + +/* SECONDARY: El Botón "Start/Select" o Gameboy Screen (Verde/Gris) */ +.secondary { + background-color: #333333; + /* Gris oscuro D-PAD */ + color: #f0f0f0; + border: 2px solid #555; + /* Un borde sutil para diferenciar */ +} + +.secondary:hover:not(:disabled) { + background-color: #444444; +} \ No newline at end of file diff --git a/src/components/ActionButton.js b/src/components/ActionButton.js new file mode 100644 index 0000000..3510528 --- /dev/null +++ b/src/components/ActionButton.js @@ -0,0 +1,17 @@ +import React from 'react'; +import './ActionButton.css'; + +//Componente reutilizable botón que permite pasar por props +//un nombre de botón, una función onClick, un estado y variantes de color +function ActionButton({ label, onClick, disabled, variant = 'primary' }) { + return ( + + ); +} +export default ActionButton; \ No newline at end of file diff --git a/src/components/PageLabel.css b/src/components/PageLabel.css new file mode 100644 index 0000000..ff7f6a7 --- /dev/null +++ b/src/components/PageLabel.css @@ -0,0 +1,38 @@ +.retro-image-label { + /* AHORA: Aumenta este valor. Prueba con 40px, 50px, etc. */ + border: 25px solid transparent; + border-image-slice: 60 fill; + /* Ancho máximo*/ + width: 100%; + + max-width: 700px; + /* Centrado horizontal en la página */ + margin: 20px auto; + /* Alineación del contenido (el texto) dentro del marco */ + display: flex; + justify-content: center; +} + + +.page-label-container { + text-align: center; + + margin-bottom: 2rem; + margin-top: 2rem; + + color: black; +} + +.page-label-text { + font-size: 1.5rem; + text-shadow: 3px 3px #ffefff; + text-transform: uppercase; + margin: 0; + font-family: 'Press Start 2P', cursive; +} + +.page-label-subtitle { + color: black; + font-size: 0.8rem; + margin-top: 10px; +} \ No newline at end of file diff --git a/src/components/PageLabel.js b/src/components/PageLabel.js new file mode 100644 index 0000000..c43e4a4 --- /dev/null +++ b/src/components/PageLabel.js @@ -0,0 +1,14 @@ +import React from 'react'; +import './PageLabel.css'; + +function PageLabel({ title, subtitle }) { + return ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ ); +} +export default PageLabel; \ No newline at end of file diff --git a/src/components/PokemonCard.css b/src/components/PokemonCard.css deleted file mode 100644 index 4fdbbed..0000000 --- a/src/components/PokemonCard.css +++ /dev/null @@ -1,26 +0,0 @@ -.card { - /* Add shadows to create the "card" effect */ - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); - transition: 0.3s; - padding: 1%; - margin: 2%; - background-color: #73BF89; - border-radius: 10%; -} - -img { - height: 10vw; - width: 10vw; -} - -/* On mouse-over, add a deeper shadow */ -.card:hover { - box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); - background-color: #337045; -} - -/* Add some padding inside the card container */ -.container { - padding: 2px 16px; - padding: 1%; -} \ No newline at end of file diff --git a/src/components/PokemonCard.js b/src/components/PokemonCard.js deleted file mode 100644 index 9ff2702..0000000 --- a/src/components/PokemonCard.js +++ /dev/null @@ -1,28 +0,0 @@ -import './PokemonCard.css' -function PokemonCard({ name, image, hp, maxHp, type }) { - - - - return ( -
- {name} - -
-

{name}

- - - Tipo: {type} - - -
- - -
-
- -
- ); -} - -export default PokemonCard; \ No newline at end of file diff --git a/src/components/TeamDisplay.css b/src/components/TeamDisplay.css new file mode 100644 index 0000000..7bbbd8b --- /dev/null +++ b/src/components/TeamDisplay.css @@ -0,0 +1,119 @@ +/* src/components/TeamDisplay.css */ + +/* --- CONTENEDOR PRINCIPAL --- */ +.team-display-container { + background: #3f445c; + border: 2px solid #455698; + border-radius: 12px; + padding: 15px; + color: white; + display: flex; + flex-direction: column; + gap: 15px; + transition: all 0.3s ease; + box-shadow: 0 0 30px rgba(41, 16, 113, 0.4); + /* Aseguramos que ocupe el 100% de la altura disponible en el grid */ + min-height: 100%; + max-height: 50%; + box-sizing: border-box; + width: 250px; +} + +/* Estado Activo (Cuando es el turno del jugador) */ +.team-display-container.active { + border-color: #455698; /* Rojo intenso */ + box-shadow: 0 0 30px rgba(41, 16, 113, 0.4); + background: #505a8a; + transform: scale(1.02); /* Pequeño efecto "pop" */ +} + +/* --- CABECERA DEL ENTRENADOR --- */ +.trainer-header { + border-bottom: 1px solid #eee9e9; + padding-bottom: 12px; +} + +.trainer-img { + width: 60%; + padding: 1.5vw; +} + +.trainer-name { + margin: 0; + font-size: 1.1rem; + text-transform: uppercase; + color: #f0f0f0; +} + +.team-count { + font-size: 0.8rem; + color: white; + margin-top: 4px; +} + +/* --- GRID DE SLOTS (La lista de Pokémon TEAM DISPLAY) --- */ +.team-slots-grid { + display: flex; + flex-direction: column; + gap: 10px; + +} + +/* Estilo base de un slot del TEAM DISPLAY (hueco) */ +.team-slot { + height: 48px; + background: #222; + border: 1px dashed #555; + border-radius: 6px; + display: flex; + align-items: center; + padding: 0 10px; + font-size: 0.9rem; + transition: all 0.2s ease; + user-select: none; +} + +/* Slot LLENO (Con Pokémon) */ +.team-slot.filled { + background: #4f4682; + border: 1px solid #2d2849; + border-style: solid; /* Cambia de dashed a solid */ + color: white; +} + +/* Efecto hover solo si está lleno y se puede clicar (cursor pointer) */ +.team-slot.filled[style*="pointer"]:hover { + background: #8d85ba; /* Fondo rojizo oscuro */ + border-color: #2c1b8f; +} + +/* Slot VACÍO -> barrita gris */ +.team-slot.empty { + justify-content: center; + color: #f3f0f0; +} + +/* --- ELEMENTOS DENTRO DEL POKEMON'BOX de TEAM DISPLAY --- */ +/*Icono pokemon*/ +.slot-icon { + width: 40px; + height: 35px; + margin-right: 12px; + image-rendering: pixelated; /* Mantiene los píxeles nítidos */ +} + +/*Nombre pokemon*/ +.slot-name { + text-transform: capitalize; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +/*Espacio sin pokemon*/ +.empty-label { + font-style: italic; + font-size: 1.2rem; + font-weight: bold; + opacity: 0.3; +} \ No newline at end of file diff --git a/src/components/TeamDisplay.js b/src/components/TeamDisplay.js new file mode 100644 index 0000000..acdeed9 --- /dev/null +++ b/src/components/TeamDisplay.js @@ -0,0 +1,101 @@ +import React from 'react'; +import { CONFIG } from '../utils/constants'; +import './TeamDisplay.css'; + +/*TODO - COMENTAR Y EXPLICAR CÓDIGO PARA DOCUMENTACIÓN*/ + +function TeamDisplay({ + player, + team = [], + isActive, // Si es el turno de este jugador (Panel iluminado) + activeIndex, // <--- NUEVO: Índice del Pokémon luchando actualmente (solo para Battle) + onSlotClick, // Función al hacer click en un slot (Borrar o Cambiar) + onPanelClick, // Función para cambiar el turno manual en Selección + className = '' +}) { + + // Rellenamos hasta llegar a 6 huecos + const pokemonBoxes = [...team, ...Array(CONFIG.MAX_TEAM_SIZE - team.length).fill(null)]; + + return ( +
+ {/** --- HEADER DEL ENTRENADOR --- **/} +
+
+ {(player?.sprite) ? ( + Avatar + ) : ( +
?
+ )} +
+
+

{player?.name || "Trainer"}

+
Pokémons: {team.length} / {CONFIG.MAX_TEAM_SIZE}
+
+
+ + {/** --- GRID DE SLOTS --- **/} +
+ {pokemonBoxes.map((poke, index) => { + + // ¿Es este el pokemon que está luchando ahora mismo? + const isBattling = index === activeIndex; + + return ( +
{ + e.stopPropagation(); // Evita seleccionar el panel entero + if (poke && onSlotClick) { + onSlotClick(index); // ¡Ejecutamos la acción! + } + }} + + style={{ + cursor: (poke && onSlotClick) ? 'pointer' : 'default', + // Opcional: Borde diferente si es el activo + borderColor: isBattling ? '#ffcc00' : undefined + }} + > + {poke ? ( + <> + {poke.name} + {poke.name} + + {/* Indicador visual de "EN COMBATE" (Opcional) */} + {isBattling &&
FIGHT
} + + {/* Barra de vida mini (Opcional para ver salud en el banquillo) */} + {poke.currentHp !== undefined && ( +
+
+
+ )} + + ) : ( + - + )} +
+ ); + })} +
+
+ ); +} + +export default TeamDisplay; \ No newline at end of file diff --git a/src/index.css b/src/index.css index ec2585e..82d69cd 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,19 @@ +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + font-family: 'Press Start 2P', cursive; + /*Fuente global*/ + overflow-x: hidden; + /* Evita scroll horizontal innecesario */ + +} + +#root { + min-height: 100vh; } + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; -} +} \ No newline at end of file diff --git a/src/resources/background.jpg b/src/resources/background.jpg deleted file mode 100644 index 2b6b33b..0000000 Binary files a/src/resources/background.jpg and /dev/null differ diff --git a/src/screens/battle/BattleScreen.css b/src/screens/battle/BattleScreen.css new file mode 100644 index 0000000..0942468 --- /dev/null +++ b/src/screens/battle/BattleScreen.css @@ -0,0 +1,22 @@ +/* BattleScreen.css */ +.battle-screen-layout { + display: flex; + justify-content: space-between; + align-items: stretch; /* Estira las columnas */ + height: 100%; /* Ocupa el contenedor padre (game-viewport) */ + padding: 20px; + gap: 20px; +} + +.side-column { + width: 300px; /* Ancho fijo para las listas */ + height: 80%; + border-radius: 12px; +} + +.center-column { + flex: 1; /* Ocupa todo el espacio sobrante */ + display: flex; + flex-direction: column; + +} \ No newline at end of file diff --git a/src/screens/battle/BattleScreen.js b/src/screens/battle/BattleScreen.js new file mode 100644 index 0000000..af3f0ac --- /dev/null +++ b/src/screens/battle/BattleScreen.js @@ -0,0 +1,97 @@ +import React from 'react'; +import { useBattleLogic } from './hooks/useBattleLogic'; +import BattleArena from './components/BattleArena'; +import TeamDisplay from '../../components/TeamDisplay'; +import ActionButton from '../../components/ActionButton'; // Reutilizamos tu botón +import './BattleScreen.css'; + +// Recibimos 'onBack' de App.js +const BattleScreen = ({ players, teams, onBack, onReset }) => { + const { + loading, + p1Pokemon, p2Pokemon, + p1ActiveIx, p2ActiveIx, // Necesitamos saber quién está activo para marcarlo en el grid + turn, + battleLogs, + winner, + handleAttack, + handleSwitch // Nueva función + } = useBattleLogic(teams.p1, teams.p2); + + if (loading || !p1Pokemon || !p2Pokemon) { + return
Loading battle...
; + } + + // --- LOGICA UI: ¿Cuándo puede interactuar un jugador? --- + // J1 puede cambiar si es su turno O si le obligan a cambiar + const canP1Interact = turn === 'p1' || turn === 'p1_forced_switch'; + // J2 puede cambiar si es su turno O si le obligan a cambiar + const canP2Interact = turn === 'p2' || turn === 'p2_forced_switch'; + + return ( +
+ + {/* --- BOTÓN ATRÁS FLOTANTE (Absoluto o en Grid) --- */} + {/* Lo pongo aquí para que aparezca arriba a la izquierda visualmente */} + + + {/* --- COLUMNA IZQUIERDA: EQUIPO J1 --- */} +
+ { + if (canP1Interact) handleSwitch('p1', index); + }} + onPanelClick={() => {}} + /> +
+ + {/* --- COLUMNA CENTRAL --- */} +
+ handleAttack(move, turn)} + turn={turn} // Pasamos el turno tal cual ('p1', 'p2_forced_switch', etc) + winner={winner} + p1Name={players?.p1?.name || "J1"} + p2Name={players?.p2?.name || "J2"} + /> + + + + +
+ + {/* --- COLUMNA DERECHA: EQUIPO J2 --- */} +
+ { + if (canP2Interact) handleSwitch('p2', index); + }} + onPanelClick={() => {}} + /> +
+
+ ); +}; + +export default BattleScreen; \ No newline at end of file diff --git a/src/screens/battle/components/BattleArena.css b/src/screens/battle/components/BattleArena.css new file mode 100644 index 0000000..c21350a --- /dev/null +++ b/src/screens/battle/components/BattleArena.css @@ -0,0 +1,24 @@ +/* BattleArena.css */ +.arena-container { + display: flex; + flex-direction: column; + height: 100%; + gap: 15px; + margin: 10px; +} + +/* El escenario ocupa el espacio sobrante (flex 1) */ +.visual-stage { + flex: 1; + position: relative; /* VITAL para que los Combatant absolute funcionen */ + background: linear-gradient(to bottom, #87CEEB 60%, #4CAF50 60%); + border-radius: 12px; + border: 4px solid #333; + overflow: hidden; /* Que los sprites no se salgan */ +} + +/* El panel de control tiene altura fija o ajustada al contenido */ +.control-area { + height: 30%; /* Ocupa el 30% inferior */ + min-height: 150px; +} \ No newline at end of file diff --git a/src/screens/battle/components/BattleArena.js b/src/screens/battle/components/BattleArena.js new file mode 100644 index 0000000..e3f155d --- /dev/null +++ b/src/screens/battle/components/BattleArena.js @@ -0,0 +1,46 @@ +// BattleArena.js +import React from 'react'; +import Combatant from './Combatant'; // <--- IMPORTAMOS TU COMPONENTE +import ControlPanel from './ControlPanel'; // <--- IMPORTAMOS TU COMPONENTE +import './BattleArena.css'; + +const BattleArena = ({ p1Pokemon, p2Pokemon, battleLogs, onAttack, turn, winner, p1Name, p2Name }) => { + + // Lógica para saber qué botones mostrar en el ControlPanel + let currentMoves = turn === 'p1' ? p1Pokemon.moves : p2Pokemon.moves; + let activeName = turn === 'p1' ? p1Name : p2Name; + + return ( +
+ + {/* 1. ZONA VISUAL (STAGE) */} +
+ {/* RIVAL (Arriba) */} + + + {/* JUGADOR (Abajo) */} + +
+ + {/* 2. ZONA DE CONTROL */} +
+ +
+
+ ); +}; +export default BattleArena; \ No newline at end of file diff --git a/src/screens/battle/components/BattleLog.css b/src/screens/battle/components/BattleLog.css new file mode 100644 index 0000000..f7864e3 --- /dev/null +++ b/src/screens/battle/components/BattleLog.css @@ -0,0 +1,24 @@ +.log-container { + background: #222; + border: 4px solid #555; + border-radius: 6px; + height: 80%; /* Altura fija */ + padding: 10px; + overflow-y: auto; /* Scroll vertical */ + font-size: 0.75rem; + color: white; + text-transform: capitalize; + +} + +.log-entry { + margin-bottom: 5px; + border-bottom: 1px solid #333; + padding-bottom: 5px; +} + +/* +.log-entry:last-child { + color: #ffff00; /* Último mensaje resaltado + font-weight: bold; +} */ \ No newline at end of file diff --git a/src/screens/battle/components/BattleLog.js b/src/screens/battle/components/BattleLog.js new file mode 100644 index 0000000..e36bf99 --- /dev/null +++ b/src/screens/battle/components/BattleLog.js @@ -0,0 +1,35 @@ +import React, { useEffect, useRef } from 'react'; +import './BattleLog.css'; + +const BattleLog = ({ logs }) => { + const logContainerRef = useRef(null); + + // Auto-scroll al div log cada vez que llega un mensaje + useEffect(() => { + if (logContainerRef.current) { + const { scrollHeight, clientHeight } = logContainerRef.current; + + // Hacemos que la posición del scroll sea igual a la altura total del contenido + // Esto lo fuerza siempre al final + logContainerRef.current.scrollTo({ + top: scrollHeight - clientHeight, + behavior: 'smooth' + }); + } + }, [logs]); + + return ( +
+
+ {logs.map((log, index) => ( +
+ {log} +
+ ))} + +
+
+ ); +}; + +export default BattleLog; \ No newline at end of file diff --git a/src/screens/battle/components/Combatant.css b/src/screens/battle/components/Combatant.css new file mode 100644 index 0000000..81f5bd2 --- /dev/null +++ b/src/screens/battle/components/Combatant.css @@ -0,0 +1,53 @@ +/* Combatant.css */ + +/* Estilos Comunes */ +.combatant-wrapper { + position: absolute; /* BattleArena (visual-stage) es relative */ + display: flex; + align-items: flex-end; /* Alineados al suelo */ + gap: 30px; + transition: all 0.5s ease; +} + +/* === VARIACIÓN RIVAL === */ +/* Posición en el escenario */ +.pos-rival { + top: 1%; + right: 10%; + /* Orden visual: [HUD] [SPRITE] */ + flex-direction: row; +} + +/* === VARIACIÓN JUGADOR === */ +/* Posición en el escenario */ +.pos-player { + bottom: 1%; + left: 10%; + /* Orden visual: [SPRITE] [HUD] (Invertido) */ + flex-direction: row-reverse; +} + +/* Estilos internos */ +.combatant-hud { + z-index: 10; +} + +.pixel-sprite { + width: 15vw; /* Responsivo */ + max-width: 200px; + image-rendering: pixelated; + animation: float 3s ease-in-out infinite; +} + +.shadow { + width: 80%; + height: 10px; + background: rgba(0,0,0,0.3); + border-radius: 50%; + margin: 0 auto; +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } +} \ No newline at end of file diff --git a/src/screens/battle/components/Combatant.js b/src/screens/battle/components/Combatant.js new file mode 100644 index 0000000..cc19c82 --- /dev/null +++ b/src/screens/battle/components/Combatant.js @@ -0,0 +1,32 @@ +// Combatant.js +import React from 'react'; +import HealthBar from './HealthBar'; +import './Combatant.css'; + +const Combatant = ({ pokemon, trainerName, isPlayer }) => { + + // Movemos la lógica de sprites AQUÍ (SRP: Single Responsibility Principle) + const getSprite = () => { + const baseUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white"; + return isPlayer ? `${baseUrl}/back/${pokemon.id}.png` : `${baseUrl}/${pokemon.id}.png`; + }; + + return ( + // Usamos una clase base + una modificadora (is-player / is-rival) +
+ + {/* HUD */} +
+
{trainerName}
+ +
+ + {/* SPRITE */} +
+ {pokemon.name} +
+
+
+ ); +}; +export default Combatant; \ No newline at end of file diff --git a/src/screens/battle/components/ControlPanel.css b/src/screens/battle/components/ControlPanel.css new file mode 100644 index 0000000..1d78023 --- /dev/null +++ b/src/screens/battle/components/ControlPanel.css @@ -0,0 +1,28 @@ +/* ControlPanel.css */ +.control-panel-container { + width: 96%; + height: 100%; /* Llena el div .control-area de BattleArena */ + background: #2c3e50; + border: 4px solid #34495e; + border-radius: 12px; + padding: 10px; + overflow: hidden; + display: flex; + gap: 10px; + +} + +.panel-section.section-log { + flex: 1; /* El log ocupa todo el espacio posible */ + min-width: 0; + min-height: 0; + +} + +.panel-section.section-actions { + width: 40%; /* Los botones ocupan el 40% */ + min-width: 230px; + display: flex; + flex-direction: column; + +} \ No newline at end of file diff --git a/src/screens/battle/components/ControlPanel.js b/src/screens/battle/components/ControlPanel.js new file mode 100644 index 0000000..804cb37 --- /dev/null +++ b/src/screens/battle/components/ControlPanel.js @@ -0,0 +1,54 @@ +import React from 'react'; +import BattleLog from './BattleLog'; +import MoveSet from './MoveSet'; +import './ControlPanel.css'; // CSS para organizar Log vs Botones + +const ControlPanel = ({ + logs, + moves, + onAttack, + turn, + activePlayerName, + winner +}) => { + + // Lógica visual: ¿Qué mostramos a la derecha? + const renderRightPanel = () => { + if (winner) { + return ( +
+ +

Winner: {winner} !!

+
+ ); + } + + return ( + +
+
{activePlayerName}
+ +
+ ); + }; + + return ( +
+ +
+ +
+ + {/* DERECHA: Botonera o Mensaje Final */} +
+ {renderRightPanel()} +
+
+ ); +}; + +export default ControlPanel; \ No newline at end of file diff --git a/src/screens/battle/components/HealthBar.css b/src/screens/battle/components/HealthBar.css new file mode 100644 index 0000000..6a1ae31 --- /dev/null +++ b/src/screens/battle/components/HealthBar.css @@ -0,0 +1,73 @@ + +.health-box { + background-color: #f8f9fa; /* Fondo cartucho claro */ + border: 3px solid #4a4a4a; + border-radius: 8px; /* Bordes redondeados retro */ + border-bottom-width: 6px; /* Efecto 3D abajo */ + padding: 10px; + width: 260px; + position: relative; + margin-bottom: 60px; + +} + +/* Triangulito decorativo para que parezca bocadillo (opcional) +.health-box::after { + content: ''; + position: absolute; + bottom: -10px; + left: 20px; + border-width: 10px 10px 0; + border-style: solid; + border-color: #4a4a4a transparent; + display: block; + width: 0; +}*/ + + /*Cabecera superior de la barra de vida*/ + .health-header { + display: flex; + justify-content: space-between; + margin-bottom: 10px; + font-weight: bold; + text-transform: capitalize; + font-size: 0.9rem; + } + +/*Contenedor de la barra de vida*/ +.bar-container { + display: flex; + align-items: center; + background: #444; + padding: 2px; + border-radius: 10px; +} + + .hp-label { + color: #ffcc00; + font-size: 0.7rem; + font-weight: bold; + margin-right: 4px; + padding-left: 4px; + } + + .bar-background { + flex-grow: 1; + height: 10px; + background-color: #555; /* Fondo de la barra vacía */ + border-radius: 5px; + overflow: hidden; /* Para que el relleno no se salga */ + } + + .bar-fill { + height: 100%; + transition: width 0.5s ease-out, background-color 0.3s ease; /* Animación suave */ + } + + /*Numeros inferiores de la barra de vida*/ +.hp-text { + text-align: right; + font-size: 0.8rem; + margin-top: 4px; + color: #333; + } \ No newline at end of file diff --git a/src/screens/battle/components/HealthBar.js b/src/screens/battle/components/HealthBar.js new file mode 100644 index 0000000..5dc5766 --- /dev/null +++ b/src/screens/battle/components/HealthBar.js @@ -0,0 +1,52 @@ +import React from 'react'; +import './HealthBar.css'; + +/** + * HealthBar: Muestra la vida restante con una barra animada. + * @param {number} current - Vida actual + * @param {number} max - Vida máxima + * @param {string} label - Nombre del Pokémon + * @param {number} level - Nivel (opcional, decorativo) + */ +const HealthBar = ({ current, max, label, level = 50 }) => { + + // 1. Calculamos el porcentaje (asegurando que esté entre 0 y 100) + const percentage = Math.max(0, Math.min(100, (current / max) * 100)); + + // 2. Lógica de colores clásica de Pokémon + let barColor = '#4caf50'; // Verde (Saludable) + if (percentage < 50) barColor = '#ffeb3b'; // Amarillo (Precaución) + if (percentage < 20) barColor = '#f44336'; // Rojo (Peligro) + + return ( + //Contenedor de vida completo +
+ {/* Cabecera: Nombre y Nivel */} +
+ {label} + Lv.{level} +
+ + {/* Contenedor de la barra */} +
+
HP
+
+
+
+
+ + {/* Texto numérico (Ej: 120/150) */} +
+ {current} / {max} +
+
+ ); +}; + +export default HealthBar; \ No newline at end of file diff --git a/src/screens/battle/components/MoveSet.css b/src/screens/battle/components/MoveSet.css new file mode 100644 index 0000000..17996fa --- /dev/null +++ b/src/screens/battle/components/MoveSet.css @@ -0,0 +1,49 @@ +.moves-grid { + display: grid; + grid-template-columns: 1fr 1fr; /* 2 columnas */ + margin-top: 20px; + gap: 7px; + + + +} + +/*BOTONES DE MOVIMIENTOS*/ +.move-btn { + background: #f8f9fa; + border: 2px solid #ccc; + border-radius: 6px; + padding: 8px; + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + transition: all 0.1s; +} + +/*HOVER BOTONES*/ +.move-btn:hover:not(:disabled) { + background: #e9ecef; + border-color: #535e96; /* Azul al pasar el ratón */ +} + + +.move-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + filter: grayscale(100%); +} + +/*DATOS MOVIMIENTOS (NOMBRE + PP)*/ +.move-name { + font-weight: bold; + text-transform: uppercase; + font-size: 0.65rem; + color: #333; +} + +.move-pp { + font-size: 0.65rem; + color: #666; + margin-top: 4px; +} diff --git a/src/screens/battle/components/MoveSet.js b/src/screens/battle/components/MoveSet.js new file mode 100644 index 0000000..2dea7ac --- /dev/null +++ b/src/screens/battle/components/MoveSet.js @@ -0,0 +1,30 @@ +import React from 'react'; +import './MoveSet.css'; + +/** + * MoveSet: Grid de 4 botones para atacar. + * @param {Array} moves - Lista de movimientos + * @param {Function} onAttack - Función al hacer click + * @param {boolean} disabled - Si es true, no se puede clickar (turno rival o fin) + */ +const MoveSet = ({ moves, onAttack, disabled }) => { + return ( + //Contenedor total de movimientos +
+ {/**MAPEAMOS LOS MOVIMIENTOS DESCARGADOS EN UN MOV POR BOTÓN**/} + {moves.map((move, index) => ( + + ))} +
+ ); +}; + +export default MoveSet; \ No newline at end of file diff --git a/src/screens/battle/hooks/useBattleLogic.js b/src/screens/battle/hooks/useBattleLogic.js new file mode 100644 index 0000000..c5a283f --- /dev/null +++ b/src/screens/battle/hooks/useBattleLogic.js @@ -0,0 +1,171 @@ +import { useState, useEffect } from 'react'; +import { calculateDamage, checkFainted, fetchRandomMoves } from '../../../utils/gameLogic'; + +export const useBattleLogic = (playerTeamProp, rivalTeamProp) => { + const [loading, setLoading] = useState(true); + + const [playerTeam, setPlayerTeam] = useState([]); + const [rivalTeam, setRivalTeam] = useState([]); + + const [p1ActiveIx, setP1ActiveIx] = useState(0); + const [p2ActiveIx, setP2ActiveIx] = useState(0); + + // turn puede ser: 'p1', 'p2', 'end', 'p1_forced_switch', 'p2_forced_switch' + const [turn, setTurn] = useState(null); + const [battleLogs, setBattleLogs] = useState([]); + const [winner, setWinner] = useState(null); + + const addLog = (msg) => setBattleLogs(prev => [...prev, msg]); + + // --- 1. INICIALIZACIÓN --- + useEffect(() => { + const initBattle = async () => { + if (!playerTeamProp || !rivalTeamProp) return; + setLoading(true); + + // Helper carga datos + const loadTeam = async (team) => Promise.all(team.map(async (p) => { + const realMoves = await fetchRandomMoves(p.rawMoves); + const speedStat = p.stats.find(s => s.stat.name === 'speed')?.base_stat || 50; + return { + ...p, + moves: realMoves, + currentHp: p.stats[0].base_stat * 3, + maxHp: p.stats[0].base_stat * 3, + speed: speedStat + }; + })); + + const [p1Loaded, p2Loaded] = await Promise.all([ + loadTeam(playerTeamProp), + loadTeam(rivalTeamProp) + ]); + + setPlayerTeam(p1Loaded); + setRivalTeam(p2Loaded); + setLoading(false); + + // Velocidad inicial + if (p1Loaded[0].speed >= p2Loaded[0].speed) { + setTurn('p1'); + addLog(`¡${p1Loaded[0].name} es más rápido!`); + } else { + setTurn('p2'); + addLog(`¡${p2Loaded[0].name} es más rápido!`); + } + }; + initBattle(); + }, [playerTeamProp, rivalTeamProp]); + + // --- 2. CAMBIO DE POKEMON --- + const handleSwitch = (playerKey, newIndex) => { + // Validaciones básicas + if (winner) return; + + let currentTeam, setTeam, activeIndex, setActiveIndex, playerName, rivalName; + + if (playerKey === 'p1') { + currentTeam = playerTeam; setTeam = setPlayerTeam; + activeIndex = p1ActiveIx; setActiveIndex = setP1ActiveIx; + playerName = "J1"; rivalName = "p2"; + } else { + currentTeam = rivalTeam; setTeam = setRivalTeam; + activeIndex = p2ActiveIx; setActiveIndex = setP2ActiveIx; + playerName = "J2"; rivalName = "p1"; + } + + // No puedes cambiar al mismo, ni a uno debilitado + if (newIndex === activeIndex) return; + if (currentTeam[newIndex].currentHp <= 0) { + addLog("¡Ese Pokémon está debilitado!"); + return; + } + + // EJECUTAR EL CAMBIO + setActiveIndex(newIndex); + addLog(`${playerName} cambió a ${currentTeam[newIndex].name}.`); + + // LÓGICA DE TURNO TRAS CAMBIO + // Caso A: Cambio Forzoso (porque murió el anterior) + if (turn === `${playerKey}_forced_switch`) { + // Te toca atacar a ti ahora + setTurn(playerKey); + } + // Caso B: Cambio Manual (Estratégico) + else if (turn === playerKey) { + // Gastas tu turno, le toca al rival + setTurn(rivalName); + } + }; + + // --- 3. ATAQUE --- + const handleAttack = (move, attackerId) => { + if (loading || winner || turn !== attackerId) return; + + let attacker, defender, setDefenderTeam, defenderTeam, defenderIx; + + if (attackerId === 'p1') { + attacker = playerTeam[p1ActiveIx]; + defender = rivalTeam[p2ActiveIx]; + defenderTeam = rivalTeam; + setDefenderTeam = setRivalTeam; + defenderIx = p2ActiveIx; + } else { + attacker = rivalTeam[p2ActiveIx]; + defender = playerTeam[p1ActiveIx]; + defenderTeam = playerTeam; + setDefenderTeam = setPlayerTeam; + defenderIx = p1ActiveIx; + } + + // Calcular daño + const { damage, effectiveness } = calculateDamage(attacker, defender, move); + const newHp = Math.max(0, defender.currentHp - damage); + + // Actualizar equipo defensor + const newDefenderTeam = [...defenderTeam]; + newDefenderTeam[defenderIx] = { ...defender, currentHp: newHp }; + setDefenderTeam(newDefenderTeam); + + // Logs + const playerName = attackerId === 'p1' ? 'J1' : 'J2'; + let msg = `${playerName}: ${attacker.name} usó ${move.name}.`; + if (effectiveness === 'super') msg += " (Eficaz!)"; + if (effectiveness === 'weak') msg += " (Resistido)"; + addLog(msg); + + // --- VERIFICAR DERROTA O CAMBIO --- + if (newHp === 0) { + addLog(`¡${defender.name} se debilitó!`); + + // ¿Quedan vivos en el equipo defensor? + const hasLivingMembers = newDefenderTeam.some(p => p.currentHp > 0); + + if (!hasLivingMembers) { + setWinner(attackerId); + setTurn('end'); + } else { + // Forzar cambio al defensor + const loserId = attackerId === 'p1' ? 'p2' : 'p1'; + setTurn(`${loserId}_forced_switch`); + addLog(`¡${loserId === 'p1' ? 'J1' : 'J2'}, elige otro Pokémon!`); + } + } else { + // Turno normal + setTurn(attackerId === 'p1' ? 'p2' : 'p1'); + } + }; + + return { + loading, + p1Pokemon: playerTeam[p1ActiveIx], + p2Pokemon: rivalTeam[p2ActiveIx], + p1ActiveIx, // Exportamos índices para saber quién está seleccionado visualmente + p2ActiveIx, + turn, + battleLogs, + winner, + handleAttack, + handleSwitch // Exportamos la nueva función + }; +}; \ No newline at end of file diff --git a/src/screens/register/AvatarSelector.css b/src/screens/register/AvatarSelector.css new file mode 100644 index 0000000..7cd3cc8 --- /dev/null +++ b/src/screens/register/AvatarSelector.css @@ -0,0 +1,38 @@ +/*DIV GENERAL QUE GUARDA LA LISTA DE ENTRENADORES*/ +.avatar-grid { + display: flex; + flex-wrap: wrap; + gap: 25px; + justify-content: center; + margin: 50px 0; +} + +/*Tamaño Avatar con tarjeta incluida*/ +.avatar-option { + border-radius: 20px; + cursor: pointer; + width: 100px; + height: 100px; + transition: transform 0.2s; +} + +/*ESCALA AL PASAR EL RATÓN POR ENCIMA DEL TRAINER*/ +.avatar-option:hover { + transform: scale(1.7); +} + +/*AVATAR SELECCIONADO*/ +.avatar-option.selected { + border-color: #ffffff; + background: #424f70; + box-shadow: -10px 5px 10px #636984; +} + +/*IMAGEN AVATAR*/ +.avatar-option img { + width: 100%; + image-rendering: pixelated; + padding: 0; + margin: 0; + +} \ No newline at end of file diff --git a/src/screens/register/AvatarSelector.js b/src/screens/register/AvatarSelector.js new file mode 100644 index 0000000..80ffe4d --- /dev/null +++ b/src/screens/register/AvatarSelector.js @@ -0,0 +1,24 @@ +//SRP +import React from 'react'; +import { TRAINER_SPRITES } from '../../utils/constants'; +import './AvatarSelector.css'; + +//Pasamos por props "selectedSprite" para que React reconozca el entrenador seleccionado + //onSelect para pasar la función que se ejecutará al hacer click +function AvatarSelector({ selectedSprite, onSelect }) { + return ( +
+ {/*Usamos nuestro array de urls para mapear cada entrenador por separado */} + {TRAINER_SPRITES.map((trainer) => ( +
onSelect(trainer.url)} + > + {trainer.name} +
+ ))} +
+ ); +} +export default AvatarSelector; \ No newline at end of file diff --git a/src/screens/register/PlayerForm.js b/src/screens/register/PlayerForm.js new file mode 100644 index 0000000..e2d2862 --- /dev/null +++ b/src/screens/register/PlayerForm.js @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { TRAINER_SPRITES } from '../../utils/constants'; +import ActionButton from '../../components/ActionButton'; +import AvatarSelector from './AvatarSelector'; +import './RegisterScreen.css'; + +//COMPONENTE QUE CONTROLA LOS DATOS NECESARIOS PARA COMPLETAR EL FORMULARIO +function PlayerForm({ initialSprite, buttonLabel, onSubmit }) { + // Estado local del formulario (antes estaba en RegisterScreen) + const [name, setName] = useState(''); + const [sprite, setSprite] = useState(initialSprite || TRAINER_SPRITES[0].url); + + const handleSubmit = () => { + // Validación interna del formulario + if (!name.trim()) return alert("¡Por favor, escribe un nombre!"); + + // Enviamos los datos limpios al padre + onSubmit({ name, sprite }); + }; + + return ( +
+ {/**Formulario Input del nombre **/} + setName(e.target.value)} + placeholder="Name" + autoFocus + /> + {/**Llamamos al componente que carga nuestro array de entrenadores y el avatar elegido**/} + + {/**Llamamos al componente botón**/} + +
+ ); +} + +export default PlayerForm; \ No newline at end of file diff --git a/src/screens/register/RegisterScreen.css b/src/screens/register/RegisterScreen.css new file mode 100644 index 0000000..d5e74c4 --- /dev/null +++ b/src/screens/register/RegisterScreen.css @@ -0,0 +1,69 @@ +/*ESTILO GLOBAL DE LA PÁGINA*/ +* { + font-family: 'Press Start 2P', cursive; +} + +/*Barra de Input*/ +.register-input { + font-size: 10px; + width: 15vw; + height: 1.5vw; + margin-left: 2.5vw; + padding: 1%; + border: 8px outset; +} + +/*Contenedor con formulario y Avatar Selector*/ +.register-container { + /*Medidas del formulario*/ + width: 100%; + height: 85%; + /* Esto centra verticalmente todo el bloque */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +/* CONTENEDOR SPLIT: + La clave del diseño -> + Aquí dentro van los dos formularios y "VS" */ +.split-screen-layout { + /* Ancho máximo del contenido */ + max-width: 1000px; + /* Flexbox para las columnas */ + display: flex; + gap: 2.5vw; +} + +/*COLUMNA DE FORMULARIO PLAYER*/ +.player-column { + flex: 2; + /* Ambos ocupan el mismo ancho */ + display: flex; + flex-direction: column; +} + +/* CLASE NUEVA: Estado Bloqueado/Terminado */ +.player-column.locked { + /* Se vuelve semitransparente */ + opacity: 0.4; + /* IMPIDE hacer click o escribir (lo bloquea) */ + pointer-events: none; +} + +/*ESTILO DIVISOR "VS"*/ +.vs-divider { + align-self: center; + font-size: 3rem; + color: #b40b0b; + text-shadow: 4px 4px 0px #000; + margin: 20px; +} + +/* Área del botón final */ +.start-game-actions { + margin-top: 10px; + text-align: center; + padding-top: 20px; +} \ No newline at end of file diff --git a/src/screens/register/RegisterScreen.js b/src/screens/register/RegisterScreen.js new file mode 100644 index 0000000..9c85ee8 --- /dev/null +++ b/src/screens/register/RegisterScreen.js @@ -0,0 +1,83 @@ +// src/screens/register/RegisterScreen.js +import React, { useState } from 'react'; +import { TRAINER_SPRITES } from '../../utils/constants'; +import PageLabel from '../../components/PageLabel'; +import ActionButton from '../../components/ActionButton'; // Importamos tu botón +import PlayerForm from './PlayerForm'; +import './RegisterScreen.css'; + +function RegisterScreen({ onComplete }) { + // Estado local para guardar los datos cuando se pulsa "READY" + const [p1Data, setP1Data] = useState(null); + const [p2Data, setP2Data] = useState(null); + + //Manejador personalizado que comprueba si los datos de los jugadores se han establecido + //Si es así, permite pasar a la siguiente fase -> onComplete + const handleStartGame = () => { + // Si ambos existen -> RegisterScreen onComplete + if (p1Data && p2Data) { + onComplete({ p1: p1Data, p2: p2Data }); + } + }; + + return ( + //TÍTULO PRESENTACIÓN DE LA PÁGINA +
+ + +
+ + {/* --- COLUMNA JUGADOR 1 --- */} + {/* Si p1Data tiene datos, añadimos la clase 'locked' */} +
+ +

+ PLAYER 1 +

Are you a boy or a girl?

+

+ + setP1Data({ ...data, id: 1 })} + /> +
+ + {/* DIVISOR VS */} +
VS
+ + {/* --- COLUMNA JUGADOR 2 --- */} +
+

+ PLAYER 2 +

Are you a boy or a girl?

+

+ setP2Data({ ...data, id: 2 })} + /> +
+ +
+ + + {/* --- BOTÓN FINAL DE CAMBIO DE PÁGINA --- */} +
+ +
+ +
+ ); +} + +export default RegisterScreen; \ No newline at end of file diff --git a/src/screens/selection/Pagination.js b/src/screens/selection/Pagination.js new file mode 100644 index 0000000..c9e7381 --- /dev/null +++ b/src/screens/selection/Pagination.js @@ -0,0 +1,48 @@ +import React from 'react'; + +// Carga estilos de la págin padre -> SelectionScreen.css +//Componente reutilizable de barra de paginación + +function Pagination({ page, totalPages, setPage, getPaginationGroup }) { + + //Manejador personal + //-> si item = "..." -> no hagas nada + //Si no, actualiza la página (setPage(item)) + const handlePageChange = (item) => { + if (item === '...') return; + setPage(item); + }; + + return ( +
+ {/**Implementamos etiqueta botón para la primera flecha izq + disabled -> si estaamos en la página 1, desactiva el botón + si clickamos -> setea la página y le resta 1**/} + + {/**El item es cada página e índice es su posición dentro del Array**/} +
+ {getPaginationGroup().map((item, index) => ( + + ))} +
+ + +
+ ); +} + +export default Pagination; \ No newline at end of file diff --git a/src/screens/selection/PokemonBox.js b/src/screens/selection/PokemonBox.js new file mode 100644 index 0000000..632c8f2 --- /dev/null +++ b/src/screens/selection/PokemonBox.js @@ -0,0 +1,27 @@ +import React from 'react'; + +// Carga estilos de la págin padre -> SelectionScreen.css +/*ToDo: Comentar el código + funcionalidades/lógica aplicada aquí*/ +function PokemonBox({ pokemons, loading, onSelect, isOwned }) { + return ( +
+ {loading ? ( +

Loading PC data....

+ /*Operador ternario*/ + ) : ( + pokemons.map((poke) => ( +
!isOwned(poke.id) && onSelect(poke)} + > + {poke.name} + {poke.name} +
+ )) + )} +
+ ); +} + +export default PokemonBox; \ No newline at end of file diff --git a/src/screens/selection/PokemonDetail.css b/src/screens/selection/PokemonDetail.css new file mode 100644 index 0000000..99ac2df --- /dev/null +++ b/src/screens/selection/PokemonDetail.css @@ -0,0 +1,171 @@ +/* PokemonDetail.css */ + +/* Overlay Background */ +.detail-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.85); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; + backdrop-filter: blur(5px); +} + +/* Gameboy Body */ +.gameboy-body { + background-color: #c4cfa1; + width: 320px; + border-radius: 10px 10px 40px 10px; + padding: 20px; + box-shadow: inset 4px 4px 10px rgba(255, 255, 255, 0.4), inset -4px -4px 10px rgba(0, 0, 0, 0.2), 0 15px 30px rgba(0, 0, 0, 0.6); + border: 3px solid #9aa67b; + display: flex; + flex-direction: column; + gap: 15px; +} + +.gameboy-top-ridge { + display: flex; + justify-content: space-between; + color: #7b8563; + font-size: 0.7rem; + font-weight: bold; + padding: 0 5px; + border-bottom: 2px solid #aeb98d; + margin-bottom: 5px; +} + +.gameboy-screen-bezel { + background-color: #555; + padding: 15px 20px 30px 20px; + border-radius: 10px 10px 30px 10px; + box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.5); +} + +.gameboy-screen-content { + background-color: #e0f0c0; + border: 3px solid #444; + border-radius: 4px; + padding: 15px; + box-shadow: inset 2px 2px 5px rgba(0, 0, 0, 0.1); + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + color: #2b331f; +} + +.gb-close-btn { + position: absolute; + top: 5px; + right: 5px; + background: none; + border: none; + font-size: 1.5rem; + line-height: 1; + color: #2b331f; + cursor: pointer; + font-weight: bold; +} + +.gb-title { + margin: 0; + font-size: 1rem; + text-align: center; + border-bottom: 2px dashed #2b331f; + width: 100%; + padding-bottom: 5px; + text-transform: uppercase; +} + +.gb-image-container { + background: rgba(43, 51, 31, 0.05); + border-radius: 50%; + padding: 10px; + border: 2px dotted #2b331f; +} + +.gb-sprite { + width: 100px; + height: 100px; + image-rendering: pixelated; +} + +.gb-info-section { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; +} + +.gb-type-tag { + background: #2b331f; + color: #e0f0c0; + text-align: center; + padding: 4px; + font-weight: bold; + border-radius: 2px; + font-size: 0.8rem; +} + +.gb-stats-container { + display: flex; + flex-direction: column; + gap: 5px; +} + +.stat-row { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 0.75rem; + font-weight: bold; +} + +.stat-bar-track { + flex-grow: 1; + height: 8px; + background: rgba(43, 51, 31, 0.2); + margin: 0 8px; + border: 1px solid #2b331f; + border-radius: 2px; +} + +.stat-bar-fill { + height: 100%; + background: #2b331f; +} + +.gameboy-controls-area { + display: flex; + justify-content: flex-end; + align-items: center; + padding-right: 10px; +} + +.gb-action-btn-a { + width: 60px; + height: 60px; + background: linear-gradient(145deg, #a83279, #85205d); + border-radius: 50%; + border: 4px solid #6e1a4d; + color: #e0f0c0; + font-weight: bold; + font-size: 1.2rem; + cursor: pointer; + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.4); + display: flex; + justify-content: center; + align-items: center; +} + +.gb-action-btn-a:active { + transform: translate(2px, 2px); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); + background: linear-gradient(145deg, #85205d, #a83279); +} \ No newline at end of file diff --git a/src/screens/selection/PokemonDetail.js b/src/screens/selection/PokemonDetail.js new file mode 100644 index 0000000..fb49fdd --- /dev/null +++ b/src/screens/selection/PokemonDetail.js @@ -0,0 +1,120 @@ +import React from 'react'; +import './PokemonDetail.css'; // Importing the dedicated styles + +/** + * PokemonDetail Component + * * Displays a detailed view of a selected Pokemon inside a "Gameboy" style modal. + * Shows stats (HP, Attack, Defense), type, and an image sprite. + * * @param {Object} props + * @param {Object} props.pokemon - The Pokemon data object containing stats, sprites, and types. + * @param {Function} props.onClose - Function to trigger when closing the modal. + * @param {Function} props.onConfirm - Function to trigger when selecting/confirming the Pokemon. + */ +function PokemonDetail({ pokemon, onClose, onConfirm }) { + // If no pokemon data is provided, do not render anything. + if (!pokemon) return null; + + /** + * Prevents the click event from bubbling up to the overlay. + * This ensures clicking the Gameboy body doesn't accidentally close the modal. + * @param {Event} e - The click event + */ + const handleCardClick = (e) => { + e.stopPropagation(); + }; + + // Helper to format types. Handles both array (standard API) or string formats. + // Example output: "FIRE / FLYING" + const typeStr = pokemon.types + ? pokemon.types.map(t => t.type.name).join(' / ').toUpperCase() + : (pokemon.type || '???').toUpperCase(); + + // Calculate stat percentage for the progress bars (Max stat assumed as 150 for visual scaling) + const getStatWidth = (statValue) => `${Math.min(100, (statValue / 150) * 100)}%`; + + return ( + // The Overlay (dark background) closes the modal when clicked +
+ + {/* The Gameboy Container */} +
+ + {/* Screen Bezel (Dark border around screen) */} +
+ + {/* Lit Screen (Actual Content) */} +
+ + {/* Close Button (Top Right) */} + + +

+ {pokemon.name} +

+ +
+ {pokemon.name} +
+ +
+
TYPE: {typeStr}
+ +
+ {/* HP Stat Bar */} +
+ HP: +
+
+
+ {pokemon.hp} +
+ + {/* Attack Stat Bar */} +
+ ATK: +
+
+
+ {pokemon.attack} +
+ + {/* Defense Stat Bar */} +
+ DEF: +
+
+
+ {pokemon.defense} +
+
+
+ +
{/* End screen-content */} +
{/* End screen-bezel */} + + {/* Bottom Control Area (A Button) */} +
+ +
+ +
+
+ ); +} + +export default PokemonDetail; \ No newline at end of file diff --git a/src/screens/selection/SelectionScreen.css b/src/screens/selection/SelectionScreen.css new file mode 100644 index 0000000..37430d3 --- /dev/null +++ b/src/screens/selection/SelectionScreen.css @@ -0,0 +1,154 @@ +*{ + font-family: 'Press Start 2P', cursive; +} + +.team-builder-layout { + display: grid; + /* 3 COLUMNAS: Sidebar Izq | Centro Flexible | Sidebar Der */ + grid-template-columns: 240px 1fr 240px; + gap: 20px; + padding: 5vh; + margin: 2vh; + height: 120vh; + font-family: 'Press Start 2P', cursive; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + background-color: #82b2df; + +} + +.sidebar-team { + /* Se colocan en la fila 2 automáticamente */ + grid-row: 2; + cursor: pointer; + +} + + +.pc-pagination-header { + display: flex; + align-items: center; + justify-content: space-between; + background: #595daa; + padding: 8px; + border-radius: 6px; + margin-bottom: 15px; + border-bottom: 3px solid #3e2694; + +} + + +.box-tab { + background: #3c5ee7; + border: none; + padding: 8px 12px; + color: white; + font-weight: bold; + font-size: 0.7em; + cursor: pointer; +} + +.box-tab:hover { + background: #6373ec; + color: white; +} + +.box-tab.active { + background: white; + color: #2b30c0; + border-bottom: 3px solid #5c4f91; + transform: translateY(-2px); + box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2); +} + +.box-tab.dots { + background: transparent; + color: white; + cursor: default; +} + +/*Flechas de la etiqueta de paginación*/ +.nav-arrow { + background: #29257c; + border: none; + border-radius: 50%; + width: 32px; + height: 32px; + cursor: pointer; + box-shadow: 0 2px 0 #837abb; + color: white; +} + + +.main-pc-container { + grid-row: 2; + /* Fila 2 */ + border-radius: 15px; + padding: 15px; + + + height: 100%; + overflow: hidden; + /* Vital para que el scroll funcione dentro */ + box-sizing: border-box; +} + + +/* ========================================= + 5. GRID POKEMON (PC BOX) + ========================================= */ +.pc-grid-compact { + display: grid; + /* Esto crea columnas automáticas de mínimo 90px */ + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 10px; + overflow-y: auto; + /* Scroll vertical */ + padding: 10px; + background: rgba(0, 0, 0, 0.15); + border-radius: 8px; +} + +/* Estilos de la tarjeta pequeña */ +.pokemon-slot-compact { + background: #fdfdfd; + border: 2px solid #555; + border-radius: 6px; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 5px; + height: 90px; + transition: transform 0.1s; +} + +.pokemon-slot-compact:hover { + border-color: black; + transform: scale(1.05); + z-index: 10; +} + +.pokemon-slot-compact.selected { + filter: grayscale(1); + opacity: 0.5; + cursor: not-allowed; +} + +.sprite-compact { + width: 80px; + height: 80px; + image-rendering: pixelated; + object-fit: contain; +} + +.name-compact { + font-size: 0.55rem; + font-weight: bold; + color: #333; + text-transform: capitalize; + text-align: center; + width: 100%; + overflow: hidden; +} \ No newline at end of file diff --git a/src/screens/selection/SelectionScreen.js b/src/screens/selection/SelectionScreen.js new file mode 100644 index 0000000..c88c348 --- /dev/null +++ b/src/screens/selection/SelectionScreen.js @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; +import './SelectionScreen.css'; +import { CONFIG } from '../../utils/constants'; + +import { usePokemonList } from './hooks/usePokemonList'; + +import PageLabel from '../../components/PageLabel'; +import ActionButton from '../../components/ActionButton'; +import TeamDisplay from '../../components/TeamDisplay'; + +import Pagination from './Pagination'; +import PokemonBox from './PokemonBox'; +import PokemonDetail from './PokemonDetail'; + +// AÑADIDO: onReset en las props +function SelectionScreen({ players, onBattleStart, onReset }) { + + //ESTADOS NECESARIOS -> EquipoJ1/EquipoJ2 -> Set del turno Actual + const [equipoP1, setEquipoP1] = useState([]); + const [equipoP2, setEquipoP2] = useState([]); + const [jugadorActivo, setJugadorActivo] = useState(1); + //Pokemon elegido para mostrar el Detalle + const [pokemonVisto, setPokemonVisto] = useState(null); + + //Estado que recibe los datos después de llamar a la API + const { + pokemons, loading, page, setPage, totalPages, getPaginationGroup + } = usePokemonList(); + + //Manejador personal que permite confirmar que añadimos pokemons a nuestro equipo + //Y gestiona al mismo tiempo la alternancia de turnos entre jugadores + const handleConfirmar = (pokemon) => { + //(Guard Clause) + if (!pokemon) return; + + //Función que gestiona el acoplamiento de un pokemon a determinado equipo + const agregarAlEquipo = (equipo, setEquipo, turnoSiguiente) => { + if (equipo.length < CONFIG.MAX_TEAM_SIZE) { + setEquipo([...equipo, pokemon]); //Añdimos el nuevo pokemon seleccionado al final del array del equipo + + //Gestionamos -> cuando un equipo esté lleno, no se alterna de turno + const elOtroEquipo = turnoSiguiente === 1 ? equipoP1 : equipoP2; + if (elOtroEquipo.length < CONFIG.MAX_TEAM_SIZE) { + setJugadorActivo(turnoSiguiente); + } + } + }; + + if (jugadorActivo === 1) { + agregarAlEquipo(equipoP1, setEquipoP1, 2); + } else { + agregarAlEquipo(equipoP2, setEquipoP2, 1); + } + setPokemonVisto(null); + }; + + //Manejador personal + const handleRemove = (playerNum, index) => { + if (playerNum === 1) { + const nuevo = [...equipoP1]; + nuevo.splice(index, 1); + setEquipoP1(nuevo); + } else { + const nuevo = [...equipoP2]; + nuevo.splice(index, 1); + setEquipoP2(nuevo); + } + // Al borrar, recuperas el turno automáticamente + setJugadorActivo(playerNum); + }; + + const isOwned = (pokeId) => { + return equipoP1.some(p => p.id === pokeId) || equipoP2.some(p => p.id === pokeId); + }; + + const isReady = equipoP1.length === CONFIG.MAX_TEAM_SIZE && + equipoP2.length === CONFIG.MAX_TEAM_SIZE; + + return ( +
+ +
+ + + onBattleStart({ p1: equipoP1, p2: equipoP2 })} + /> + + {/* 2. LATERAL IZQUIERDO */} + handleRemove(1, idx)} + // NUEVO: Click para cambiar turno + onPanelClick={() => setJugadorActivo(1)} + /> + + {/* 3. CENTRO (PC) */} +
+ + + +
+ + {/* 4. LATERAL DERECHO */} + handleRemove(2, idx)} + // NUEVO: Click para cambiar turno + onPanelClick={() => setJugadorActivo(2)} + /> + + {/* MODAL */} + setPokemonVisto(null)} + onConfirm={handleConfirmar} + /> +
+
+ ); +} + +export default SelectionScreen; \ No newline at end of file diff --git a/src/screens/selection/hooks/usePokemonList.js b/src/screens/selection/hooks/usePokemonList.js new file mode 100644 index 0000000..9ad917f --- /dev/null +++ b/src/screens/selection/hooks/usePokemonList.js @@ -0,0 +1,92 @@ +import { useState, useEffect } from 'react'; +import { CONFIG } from '../../../utils/constants'; + +export const usePokemonList = () => { + const [pokemons, setPokemons] = useState([]); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + + const { API_LIMIT, MAX_POKEMON_ID, TOTAL_BOXES } = CONFIG; + + // --- LÓGICA DE PAGINACIÓN ACTUALIZADA --- + const getPaginationGroup = () => { + //Variables necesarias -> + const total = TOTAL_BOXES; + const current = page; + const delta = 1; // Cuántos mostrar a izquierda y derecha + const range = []; + //ALGORITMO DE CÁLCULO -> CUANTO DRCH / IZQ + const left = Math.max(2, current - delta); + const right = Math.min(total - 1, current + delta); + // 1. SIEMPRE añadimos la Caja 1 + range.push(1); + // 2. Puntos suspensivos IZQUIERDA + // Si entre el 1 y mi rango izquierdo hay hueco (ej: 1 ... 4) + if (left > 2) { + range.push('...'); + } + // 3. Añadimos el rango central (Vecinos + Actual) + for (let i = left; i <= right; i++) { + range.push(i); + } + // 4. Puntos suspensivos DERECHA + // Si entre mi rango derecho y el final hay hueco (ej: 8 ... 14) + if (right < total - 1) { + range.push('...'); + } + // 5. SIEMPRE añadimos la Última Caja (si hay más de una) + if (total > 1) { + range.push(total); + } + return range; + }; + + // --- CARGA DE DATOS (Igual que antes) --- + useEffect(() => { + const cargarDatos = async () => { + setLoading(true); + try { + const offset = (page - 1) * API_LIMIT; + const url = `https://pokeapi.co/api/v2/pokemon?limit=${API_LIMIT}&offset=${offset}`; + const respuesta = await fetch(url); + const datosLista = await respuesta.json(); + + const promesasDetalles = datosLista.results.map(async (poke) => { + const partesUrl = poke.url.split('/'); + const id = parseInt(partesUrl[partesUrl.length - 2]); + if (id > MAX_POKEMON_ID) return null; + + const resDetalle = await fetch(poke.url); + const data = await resDetalle.json(); + + return { + id: data.id, + name: data.name, + image: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-vii/icons/${data.id}.png`, + sprite: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-v/black-white/${data.id}.png`, + sprites: data.sprites, + types: data.types, + stats: data.stats, + rawMoves: data.moves, + hp: data.stats[0].base_stat, + maxHp: data.stats[0].base_stat, + attack: data.stats[1].base_stat, + defense: data.stats[2].base_stat + }; + }); + + const pokemonsCompletos = await Promise.all(promesasDetalles); + setPokemons(pokemonsCompletos.filter(p => p !== null)); + } catch (error) { + console.error("Error:", error); + } finally { + setLoading(false); + } + }; + cargarDatos(); + }, [page, API_LIMIT, MAX_POKEMON_ID]); + + return { + pokemons, loading, page, setPage, totalPages: TOTAL_BOXES, getPaginationGroup + }; +}; \ No newline at end of file diff --git a/src/utils/constants.js b/src/utils/constants.js new file mode 100644 index 0000000..44c8ab2 --- /dev/null +++ b/src/utils/constants.js @@ -0,0 +1,36 @@ +//CONSTANTES GLOBALES DEL PROYECTO + +//FASES DEL JUEGO (Pantallas) +export const GAME_PHASES = { + REGISTER: 'REGISTER', + SELECTION: 'SELECTION', + BATTLE: 'BATTLE' +}; + +//CONFIGURACIÓN FIJA +export const CONFIG = { + MAX_TEAM_SIZE: 6, + API_LIMIT: 48, // Para paginación + MAX_POKEMON_ID: 649, // Hasta Gen 5 + + //División manual para que muestre el número de boxes establecido + get TOTAL_BOXES() { + return Math.ceil(this.MAX_POKEMON_ID / this.API_LIMIT); + } +}; + +//Al no tener una API de propio, importamos de forma manual los urls de los avatares que queremos +export const TRAINER_SPRITES = [ + // GEN 2 (Johto - HGSS) + { id: 'ethan', name: 'Ethan', url: 'https://play.pokemonshowdown.com/sprites/trainers/ethan.png' }, + { id: 'lyra', name: 'Lyra', url: 'https://play.pokemonshowdown.com/sprites/trainers/lyra.png' }, + // GEN 3 (Hoenn - ORAS/Emerald) + { id: 'brendan', name: 'Brendan', url: 'https://play.pokemonshowdown.com/sprites/trainers/brendan.png' }, + { id: 'may', name: 'May', url: 'https://play.pokemonshowdown.com/sprites/trainers/may.png' }, + // GEN 4 (Sinnoh - Platinum/DP) + { id: 'lucas', name: 'Lucas', url: 'https://play.pokemonshowdown.com/sprites/trainers/lucas.png' }, + { id: 'dawn', name: 'Dawn', url: 'https://play.pokemonshowdown.com/sprites/trainers/dawn.png' }, + // GEN 5 (Unova/Teselia - BW) + { id: 'hilbert', name: 'Hilbert', url: 'https://play.pokemonshowdown.com/sprites/trainers/hilbert.png' }, + { id: 'hilda', name: 'Hilda', url: 'https://play.pokemonshowdown.com/sprites/trainers/hilda.png' }, +]; \ No newline at end of file diff --git a/src/utils/gameLogic.js b/src/utils/gameLogic.js new file mode 100644 index 0000000..e04f849 --- /dev/null +++ b/src/utils/gameLogic.js @@ -0,0 +1,113 @@ +// src/utils/gameLogic.js + +// ========================================== +// 1. TABLA DE TIPOS (TYPE CHART) +// ========================================== +const TYPE_CHART = { + fire: { strong: ['grass', 'ice', 'bug', 'steel'], weak: ['water', 'fire', 'rock', 'dragon'] }, + water: { strong: ['fire', 'ground', 'rock'], weak: ['water', 'grass', 'dragon'] }, + grass: { strong: ['water', 'ground', 'rock'], weak: ['fire', 'grass', 'poison', 'flying', 'bug', 'dragon'] }, + electric: { strong: ['water', 'flying'], weak: ['electric', 'grass', 'dragon', 'ground'] }, + psychic: { strong: ['fighting', 'poison'], weak: ['psychic', 'steel'] }, + ice: { strong: ['grass', 'ground', 'flying', 'dragon'], weak: ['fire', 'water', 'ice', 'steel'] }, + dragon: { strong: ['dragon'], weak: ['steel'] }, + normal: { strong: [], weak: ['rock', 'steel'] }, + fighting: { strong: ['normal', 'ice', 'rock', 'dark', 'steel'], weak: ['poison', 'flying', 'psychic', 'bug'] }, + flying: { strong: ['grass', 'fighting', 'bug'], weak: ['electric', 'rock', 'steel'] }, + poison: { strong: ['grass', 'fairy'], weak: ['poison', 'ground', 'rock', 'ghost'] }, + ground: { strong: ['fire', 'electric', 'poison', 'rock', 'steel'], weak: ['grass', 'bug'] }, + rock: { strong: ['fire', 'ice', 'flying', 'bug'], weak: ['fighting', 'ground', 'steel'] }, + bug: { strong: ['grass', 'psychic', 'dark'], weak: ['fire', 'fighting', 'poison', 'flying', 'ghost', 'steel'] }, + ghost: { strong: ['psychic', 'ghost'], weak: ['dark'] }, + steel: { strong: ['ice', 'rock', 'fairy'], weak: ['fire', 'water', 'electric', 'steel'] } +}; + +// ========================================== +// 2. CÁLCULO DE DAÑO +// ========================================== +export const calculateDamage = (attacker, defender, move) => { + // Si faltan datos, no hacemos daño + if (!attacker || !defender) return { damage: 0, effectiveness: 'normal' }; + + // Obtenemos el tipo del ataque (move.type) y del defensor (types[0]) + const attackType = move.type || 'normal'; + const defenderType = defender.types?.[0]?.type?.name || 'normal'; + + // Obtenemos el ataque del Pokémon (o 10 por defecto) + const attackStat = attacker.attack || 10; + + let multiplier = 1; + let effectiveness = "normal"; + + // Buscamos debilidades/fortalezas en la tabla + const typeInfo = TYPE_CHART[attackType]; + + if (typeInfo) { + if (typeInfo.strong?.includes(defenderType)) { + multiplier = 2; + effectiveness = "super"; + } else if (typeInfo.weak?.includes(defenderType)) { + multiplier = 0.5; + effectiveness = "weak"; + } + } + + // Fórmula simple de daño: + // (Poder del ataque + un poco del ataque del Pokémon) * multiplicador de tipo + const basePower = move.power || 40; + const rawDamage = (basePower + (attackStat / 5)) * multiplier; + + return { + // Aseguramos que siempre haga al menos 1 de daño + damage: Math.floor(Math.max(1, rawDamage)), + effectiveness: effectiveness + }; +}; + +// Función simple para saber si el Pokémon ha caído +export const checkFainted = (hp) => { + return hp <= 0; +}; + +// ========================================== +// 3. OBTENER MOVIMIENTOS ALEATORIOS (API) +// ========================================== +export const fetchRandomMoves = async (allMoves) => { + // 1. Si el Pokémon no tiene movimientos (raro, pero posible), devolvemos uno por defecto + if (!allMoves || allMoves.length === 0) { + return [{ name: 'Combate', type: 'normal', power: 50, accuracy: 100, pp: 35 }]; + } + + // 2. Barajar la lista completa y elegir 4 + // Esto es muy rápido porque solo movemos referencias + const selectedMoves = allMoves + .sort(() => 0.5 - Math.random()) + .slice(0, 4); + + // 3. Descargar los detalles de esos 4 movimientos desde la API + const promises = selectedMoves.map(async (moveSlot) => { + try { + const res = await fetch(moveSlot.move.url); + const data = await res.json(); + + // Intentamos buscar el nombre en español ('es'), si no, usamos el inglés + const spanishName = data.names.find(n => n.language.name === 'es'); + + return { + name: spanishName ? spanishName.name : data.name, + type: data.type.name, + // Si el poder es null (ataque de estado), le ponemos 10 para que haga algo de daño en la demo + power: data.power || 10, + accuracy: data.accuracy || 100, + pp: data.pp + }; + } catch (error) { + console.error("Error cargando movimiento:", error); + return null; // Si falla uno, devolvemos null + } + }); + + // 4. Esperar a que terminen las 4 descargas y filtrar los errores + const results = await Promise.all(promises); + return results.filter(m => m !== null); +}; \ No newline at end of file diff --git a/toDo.txt b/toDo.txt new file mode 100644 index 0000000..5dc7503 --- /dev/null +++ b/toDo.txt @@ -0,0 +1,16 @@ + +SelectionScreen: +-> Add remove button for selected pokemons (check) -> need's UI ("X" icon) +-> Modify Pokemon Detail (make it bigger) (check) +-> Add Control Exception: press start battle button -> if not pokemon selected, cant do it + + +General: +-> Change screen dimensions / resolution (too much bigger at 100%) +-> Add navegation paths (check more or less) + + +BattleScreen: +-> Reubicate BattleScreen buttons +-> Reubicate TeamDisplay view (right is closer to ) +-> Fix battleScreen bug, each move you use makes the page bigger??? \ No newline at end of file