+ );
}
-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}
-
-
- 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) ? (
+
+ ) : (
+
?
+ )}
+
+
+
{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}
+
+ {/* 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 --- */}
+
+ );
+};
+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 */}
+
+
+
+
+
+ );
+};
+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)}
+ >
+
+
+ ))}
+
+ );
+}
+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**/}
+