<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pokémon Battle 3D Demo - Fixed</title>
<style>
/* --- Global Styles & Material Inspired UI --- */
@import u rl('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'); /* Roboto is standard Material font */
@import u rl('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); /* Keep Inter as fallback/option */
:root {
--bg-color: #f5f5f5; /* Material Light Background */
--card-bg: #ffffff;
--text-color: rgba(0, 0, 0, 0.87); /* Material Primary Text */
--subtle-text: rgba(0, 0, 0, 0.6); /* Material Secondary Text */
--very-subtle-text: rgba(0, 0, 0, 0.38); /* Material Hint/Disabled Text */
--divider-color: rgba(0, 0, 0, 0.12); /* Material Divider */
--primary-color: #6200EE; /* Material Purple (Example) */
--primary-variant: #3700B3;
--secondary-color: #03DAC6; /* Material Teal (Example) */
--primary-blue: #2196F3; /* Material Blue */
--hover-blue: #1976D2;
--hp-high: #4CAF50; /* Material Green */
--hp-medium: #FFC107; /* Material Amber */
--hp-low: #F44336; /* Material Red */
--info-box-bg: rgba(255, 255, 255, 0.85); /* Keep refined info box bg */
--info-box-border: rgba(0, 0, 0, 0.06); /* Even subtler border */
--health-bar-track: rgba(0, 0, 0, 0.1);
-- 'Roboto', 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; /* Prioritize Roboto */
/* Card Specific */
--card-border-radius: 12px; /* Softer radius */
--card-shadow: 0 2px 4px rgba(0,0,0,0.1), 0 1px 6px rgba(0,0,0,0.08); /* Base shadow */
--card-shadow-hover: 0 6px 12px rgba(0,0,0,0.15), 0 4px 8px rgba(0,0,0,0.12); /* Hover shadow */
/* Type Chip Colors (Material Style) - ADD MORE AS NEEDED */
--type-color-electric: #FFC107; --type-text-electric: rgba(0,0,0,0.87);
--type-color-fire: #FF5722; --type-text-fire: white;
--type-color-water: #03A9F4; --type-text-water: white;
--type-color-grass: #8BC34A; --type-text-grass: rgba(0,0,0,0.87);
--type-color-ghost: #7E57C2; --type-text-ghost: white;
--type-color-psychic: #EC407A; --type-text-psychic: white;
--type-color-fighting: #A1887F; --type-text-fighting: white;
--type-color-normal: #BDBDBD; --type-text-normal: rgba(0,0,0,0.87);
--type-color-default: #9E9E9E; --type-text-default: white;
}
* { box-sizing: border-box; 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
body {
var(--font-family); background-color: var(--bg-color); color: var(--text-color);
display: flex; justify-content: center; align-items: center; padding: 20px; perspective: 1200px;
}
.game-container {
width: 100%; max-width: 850px; /* Slightly wider */ height: 90vh; max-height: 700px; /* Slightly taller */
background-color: var(--card-bg); border-radius: 18px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1); /* Softer container shadow */
overflow: hidden; position: relative; display: flex; flex-direction: column;
}
/* --- Screens --- */
.screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; padding: 0; transition: transform 0.6s ease-in-out, opacity 0.5s ease-in-out; backface-visibility: hidden; display: flex; flex-direction: column; z-index: 5; }
.selection-screen { z-index: 10; opacity: 1; transform: rotateY(0deg); align-items: center; padding: 24px; background-color: var(--bg-color); /* Match body bg */ }
.battle-screen { z-index: 5; opacity: 0; transform: rotateY(180deg); justify-content: space-between; }
.game-container.battle-active .selection-screen { opacity: 0; transform: rotateY(-180deg); pointer-events: none; }
.game-container.battle-active .battle-screen { opacity: 1; transform: rotateY(0deg); z-index: 10; }
/* --- Selection Screen --- */
.selection-screen h1 {
font-size: 24px; font-weight: 500; /* Material Title */ 24px;
color: var(--text-color); text-align: center; width: 100%;
}
.pokemon-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); /* Adjusted size */
gap: 24px; /* Material Grid Spacing */
width: 100%;
overflow-y: auto;
padding: 0 8px 16px 8px; /* Padding around grid */
max-height: calc(100% - 60px); /* Adjust calc based on h1 */
}
/* --- === POKEMON CARD - MATERIAL DESIGN === --- */
.pokemon-card {
background-color: var(--card-bg);
border-radius: var(--card-border-radius);
box-shadow: var(--card-shadow);
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
cursor: pointer;
overflow: hidden; /* Clip image and ripple */
display: flex;
flex-direction: column;
position: relative; /* For ripple */
}
.pokemon-card:hover {
transform: translateY(-4px);
box-shadow: var(--card-shadow-hover);
}
.card-image-container {
height: 140px; /* Fixed height for image area */
background-color: #e0e0e0; /* Placeholder background */
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 10px; /* Padding around the image */
position: relative; /* For potential type-color backgrounds */
}
/* Add type-specific backgrounds to image container */
.pokemon-card.type-fire .card-image-container { background-color: #FFCCBC; }
.pokemon-card.type-water .card-image-container { background-color: #B3E5FC; }
.pokemon-card.type-grass .card-image-container { background-color: #C8E6C9; }
.pokemon-card.type-electric .card-image-container { background-color: #FFF9C4; }
.pokemon-card.type-ghost .card-image-container { background-color: #D1C4E9; }
/* Add more type backgrounds */
.card-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
filter: drop-shadow(0 2px 3px rgba(0,0,0,0.2));
}
.card-content {
padding: 16px;
flex-grow: 1; /* Allows description to push footer down if needed */
display: flex;
flex-direction: column;
}
.card-name {
font-size: 18px; /* Material Subtitle 1 */
font-weight: 500;
color: var(--text-color);
8px;
1.4;
}
.card-info {
display: flex;
align-items: center;
justify-content: space-between; /* Pushes type and HP apart */
12px;
}
.card-type-chip {
display: inline-block;
padding: 4px 10px;
border-radius: 16px; /* Pill shape */
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
/* Default colors - overridden by specific type classes below */
background-color: var(--type-color-default);
color: var(--type-text-default);
}
/* Apply type-specific colors */
.type-electric .card-type-chip { background-color: var(--type-color-electric); color: var(--type-text-electric); }
.type-fire .card-type-chip { background-color: var(--type-color-fire); color: var(--type-text-fire); }
.type-water .card-type-chip { background-color: var(--type-color-water); color: var(--type-text-water); }
.type-grass .card-type-chip { background-color: var(--type-color-grass); color: var(--type-text-grass); }
.type-ghost .card-type-chip { background-color: var(--type-color-ghost); color: var(--type-text-ghost); }
.type-psychic .card-type-chip { background-color: var(--type-color-psychic); color: var(--type-text-psychic); }
.type-fighting .card-type-chip { background-color: var(--type-color-fighting); color: var(--type-text-fighting); }
.type-normal .card-type-chip { background-color: var(--type-color-normal); color: var(--type-text-normal); }
/* Add more type rules here */
.card-hp {
font-size: 14px;
font-weight: 500;
color: var(--subtle-text);
}
.card-description {
font-size: 13px; /* Material Body 2 */
font-weight: 400;
color: var(--subtle-text);
1.5;
auto; /* Pushes description down if content above is short */
padding-top: 8px; /* Space above description if needed */
/* Limit description lines */
display: -webkit-box;
-webkit-line-clamp: 2; /* Show max 2 lines */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
/* Material Ripple Effect */
.card-ripple {
position: absolute;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.1); /* Ripple color */
transform: scale(0);
opacity: 1;
pointer-events: none; /* Important */
}
.card-ripple.active {
transform: scale(2);
opacity: 0;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
/* --- === END MATERIAL CARD === --- */
/* --- Battle Screen --- */
.battle-arena { flex-grow: 1; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; background: linear-gradient(135deg, #e3f2fd 0%, #f5f5f5 100%); /* Adjusted gradient to match MD bg */ }
#three-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; pointer-events: none; display: none; }
.game-container.battle-active #three-canvas { display: block; }
/* --- Info Boxes Positioning (Keep refined Apple style - it still looks good) --- */
.opponent-side, .player-side { position: absolute; width: 240px; z-index: 4; }
.opponent-side { top: 30px; left: 30px; }
.player-side { bottom: 30px; right: 30px; }
.pokemon-info-box { background-color: var(--info-box-bg); backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(180%); border: 1px solid var(--info-box-border); border-radius: 16px; padding: 14px 20px; width: 100%; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.07); transition: background-color 0.3s ease, box-shadow 0.3s ease; }
.opponent-side .pokemon-info-box { text-align: left; }
.player-side .pokemon-info-box { text-align: right; }
.pokemon-name { font-size: 19px; font-weight: 600; color: var(--text-color); 4px; letter-spacing: 0.2px; }
.hp-text { font-size: 12px; font-weight: 500; color: var(--very-subtle-text); 8px; 1.3; }
.health-bar-container { width: 100%; height: 12px; background-color: var(--health-bar-track); border-radius: 7px; overflow: hidden; position: relative; }
.health-bar { height: 100%; width: 100%; border-radius: 7px; background-color: var(--hp-high); transition: width 0.6s cubic-bezier(0.25, 0.1, 0.25, 1), background-color 0.5s ease; }
.health-bar.medium { background-color: var(--hp-medium); }
.health-bar.low { background-color: var(--hp-low); }
/* --- Sprite Positioning --- */
.pokemon-sprite { position: absolute; width: 145px; height: 145px; object-fit: contain; transition: transform 0.3s ease; image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges; z-index: 3; filter: drop-shadow(0 5px 8px rgba(0,0,0,0.28)); transform-origin: bottom center; }
#opponent-sprite { top: 12%; right: 15%; }
#player-sprite { bottom: 8%; left: 15%; }
/* --- Battle Controls --- */
.battle-controls { display: flex; border-top: 1px solid var(--divider-color); height: 130px; z-index: 11; background-color: var(--card-bg); position: relative; }
.battle-message { flex-grow: 1; padding: 20px; font-size: 16px; 1.5; display: flex; align-items: center; overflow-y: auto; height: 100%; color: var(--text-color); }
.attack-options { width: 40%; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 15px; border-left: 1px solid var(--divider-color); height: 100%; }
.attack-button { /* Using Material Filled Button Style */
background-color: var(--primary-blue); color: white; border: none; border-radius: 4px; /* Standard MD radius */
padding: 8px 16px; /* MD Button Padding */ font-size: 14px; font-weight: 500; /* MD Button Font */
text-transform: uppercase; letter-spacing: 0.8px; /* MD Button Text Style */
cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.1s ease; text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); /* Subtle button shadow */
overflow: hidden; position: relative; /* For potential ripple */
}
.attack-button:hover:not(:disabled) { background-color: var(--hover-blue); box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); }
.attack-button:active:not(:disabled) { transform: scale(0.98); box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
.attack-button:disabled { background-color: rgba(0,0,0,0.12); color: var(--very-subtle-text); cursor: not-allowed; box-shadow: none; }
/* --- Animations --- */
.sprite-hit-flash { animation: flash 0.4s ease-out; }
.sprite-faint { animation: faint 1s ease-out forwards; }
.sprite-shake-short { animation: shake-short 0.3s ease-in-out; }
@keyframes flash { 0%, 100% { opacity: 1; filter: drop-shadow(0 5px 8px rgba(0,0,0,0.28)) brightness(1); } 50% { opacity: 0.6; filter: drop-shadow(0 5px 8px rgba(0,0,0,0.28)) brightness(1.8); } }
@keyframes faint { 0% { transform: translateY(0) scale(1); opacity: 1; } 50% { transform: translateY(-15px) rotateZ(-5deg) scale(1.05); opacity: 0.8; } 100% { transform: translateY(40px) scale(0.7); opacity: 0; } }
@keyframes shake-short { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
/* --- Game Over Screen --- */
.game-over-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(33, 33, 33, 0.7); /* Darker overlay */ backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 20; opacity: 0; pointer-events: none; transition: opacity 0.5s ease; text-align: center; }
.game-over-overlay.visible { opacity: 1; pointer-events: auto; }
.game-over-overlay h2 { font-size: 34px; 20px; font-weight: 500; }
.game-over-overlay button { /* Re-style button to match MD */
background-color: var(--secondary-color); /* Use secondary color */ color: rgba(0,0,0,0.87); border: none; border-radius: 4px;
padding: 10px 24px; font-size: 14px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.8px;
cursor: pointer; transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.1s ease; 24px;
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
}
.game-over-overlay button:hover { background-color: #00bfa5; /* Darker secondary */ box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); transform: translateY(-1px); }
.game-over-overlay button:active { transform: scale(0.99); box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
</style>
<!-- ADD THREE.JS LIBRARY -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
<div class="game-container" id="game-container">
<!-- ===== Selection Screen ===== -->
<div class="screen selection-screen" id="selection-screen">
<h1>Choose your Pokémon</h1>
<div class="pokemon-cards" id="pokemon-cards"></div>
</div>
<!-- ===== Battle Screen ===== -->
<div class="screen battle-screen" id="battle-screen">
<div class="battle-arena" id="battle-arena">
<!-- 3D Canvas -->
<canvas id="three-canvas"></canvas>
<!-- Sprites (Now direct children of Arena for easier absolute positioning) -->
<img src="" alt="Opponent Pokémon" id="opponent-sprite" class="pokemon-sprite">
<img src="" alt="Player Pokémon" id="player-sprite" class="pokemon-sprite">
<!-- Info Boxes (Absolutely positioned containers) -->
<div class="opponent-side">
<div class="pokemon-info-box">
<div class="pokemon-name" id="opponent-name">Opponent</div>
<div class="hp-text" id="opponent-hp-text">HP: ??? / ???</div>
<div class="health-bar-container">
<div class="health-bar" id="opponent-health-bar"></div>
</div>
</div>
</div>
<div class="player-side">
<div class="pokemon-info-box">
<div class="pokemon-name" id="player-name">Player</div>
<div class="hp-text" id="player-hp-text">HP: ??? / ???</div>
<div class="health-bar-container">
<div class="health-bar" id="player-health-bar"></div>
</div>
</div>
</div>
</div>
<div class="battle-controls">
<div class="battle-message" id="battle-message">Select a Pokémon!</div>
<div class="attack-options" id="attack-options"></div>
</div>
</div>
<!-- ===== Game Over Overlay ===== -->
<div class="game-over-overlay" id="game-over-overlay">
<h2 id="game-over-message">Game Over</h2>
<button id="play-again-button">Play Again</button>
</div>
</div>
<script>
// --- DOM Elements ---
const gameContainer = document.getElementById('game-container');
const selectionScreen = document.getElementById('selection-screen');
const battleScreen = document.getElementById('battle-screen');
const pokemonCardsContainer = document.getElementById('pokemon-cards');
const battleArena = document.getElementById('battle-arena');
const threeCanvas = document.getElementById('three-canvas');
const battleMessage = document.getElementById('battle-message');
const attackOptionsContainer = document.getElementById('attack-options');
// Opponent UI
const opponentNameEl = document.getElementById('opponent-name');
const opponentHpTextEl = document.getElementById('opponent-hp-text');
const opponentHealthBarEl = document.getElementById('opponent-health-bar');
const opponentSpriteEl = document.getElementById('opponent-sprite');
// Player UI
const playerNameEl = document.getElementById('player-name');
const playerHpTextEl = document.getElementById('player-hp-text');
const playerHealthBarEl = document.getElementById('player-health-bar');
const playerSpriteEl = document.getElementById('player-sprite');
// Game Over UI
const gameOverOverlay = document.getElementById('game-over-overlay');
const gameOverMessage = document.getElementById('game-over-message');
const playAgainButton = document.getElementById('play-again-button');
// --- Game Data ---
// Ensure all attacks have a valid effectType
const pokemonData = [
{
id: 1, name: 'Pikachu', type: 'Electric', hp: 90, maxHp: 90,
description: "Its electric pouches store energy. It discharges electricity when threatened.", // Added description
spriteUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png',
attacks: [ { name: 'Thunder Shock', damage: 20, effectType: 'electric' }, { name: 'Quick Attack', damage: 15, effectType: 'physical_dash' }, { name: 'Iron Tail', damage: 25, effectType: 'physical_impact' }, { name: 'Thunderbolt', damage: 35, effectType: 'electric_strong' } ]
},
{
id: 2, name: 'Charizard', type: 'Fire', hp: 120, maxHp: 120,
description: "Spits fire hot enough to melt boulders. Known to cause forest fires unintentionally.", // Added description
spriteUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png',
attacks: [ { name: 'Flamethrower', damage: 30, effectType: 'fire' }, { name: 'Slash', damage: 22, effectType: 'physical_impact' }, { name: 'Dragon Claw', damage: 28, effectType: 'physical_impact' }, { name: 'Air Slash', damage: 25, effectType: 'physical_dash' } ]
},
{
id: 3, name: 'Bulbasaur', type: 'Grass', hp: 100, maxHp: 100,
description: "A strange seed was planted on its back at birth. The plant sprouts and grows with this Pokémon.", // Added description
spriteUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png',
attacks: [ { name: 'Vine Whip', damage: 20, effectType: 'physical_impact' }, { name: 'Tackle', damage: 15, effectType: 'physical_dash' }, { name: 'Seed Bomb', damage: 28, effectType: 'grass_bomb' }, { name: 'Razor Leaf', damage: 25, effectType: 'grass_leaf' } ]
},
{
id: 4, name: 'Squirtle', type: 'Water', hp: 100, maxHp: 100,
description: "After birth, its back swells and hardens into a shell. Powerfully sprays foam from its mouth.", // Added description
spriteUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/7.png',
attacks: [ { name: 'Water Gun', damage: 20, effectType: 'water' }, { name: 'Tackle', damage: 15, effectType: 'physical_dash' }, { name: 'Bubble Beam', damage: 26, effectType: 'water_bubble' }, { name: 'Aqua Tail', damage: 30, effectType: 'physical_impact' } ]
},
{
id: 5, name: 'Gengar', type: 'Ghost', hp: 110, maxHp: 110,
description: "It hides in shadows. It is said that if Gengar is hiding, it cools the area by nearly 10°F.", // Added description
spriteUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/94.png',
attacks: [ { name: 'Shadow Ball', damage: 32, effectType: 'dark_ball' }, { name: 'Lick', damage: 18, effectType: 'physical_impact' }, { name: 'Dark Pulse', damage: 28, effectType: 'dark_pulse' }, { name: 'Sludge Bomb', damage: 30, effectType: 'poison_bomb' } ]
},
];
// --- Game State ---
let playerPokemon = null;
let opponentPokemon = null;
let isPlayerTurn = true;
let isBattleOver = false;
let attackInProgress = false; // Crucial flag
// --- Three.js Setup ---
let scene, camera, renderer, clock;
let activeParticles = [];
let animationFrameId = null;
function initThree() {
scene = new THREE.Scene();
clock = new THREE.Clock();
const arenaRect = battleArena.getBoundingClientRect();
const aspect = arenaRect.width / arenaRect.height;
camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
camera.position.set(0, 1.5, 6); // Adjusted camera slightly
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({ canvas: threeCanvas, alpha: true, antialias: true });
renderer.setSize(arenaRect.width, arenaRect.height);
renderer.setPixelRatio(window.devicePixelRatio);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(5, 10, 7);
scene.add(directionalLight);
window.addEventListener('resize', onWindowResize, false);
}
// --- Helper Functions for Card Generation ---
function mapHpToStars(hp) {
// Simple mapping: 1 star per 15 HP, max 10 stars
const stars = Math.min(10, Math.max(1, Math.ceil(hp / 15)));
return '★'.repeat(stars); // Repeat the star character
}
function getTypeAttribute(type) {
// Basic mapping of Pokemon types to Yu-Gi-Oh style attributes/colors
// This is simplified - Yu-Gi-Oh attributes are LIGHT, DARK, EARTH, WATER, FIRE, WIND, DIVINE
// We'll map Pokemon types conceptually for visual flair
switch (type.toLowerCase()) {
case 'fire': return { symbol: '🔥', color: '#FF4500' }; // FIRE - OrangeRed
case 'water': return { symbol: '💧', color: '#1E90FF' }; // WATER - DodgerBlue
case 'grass': return { symbol: '🌿', color: '#228B22' }; // EARTH (conceptually) - ForestGreen
case 'electric': return { symbol: '⚡', color: '#FFD700' }; // LIGHT (conceptually) - Gold
case 'ghost': return { symbol: '👻', color: '#4B0082' }; // DARK (conceptually) - Indigo
case 'psychic': return { symbol: '👁️', color: '#FF1493' }; // LIGHT (conceptually) - DeepPink
case 'fighting': return { symbol: '🥊', color: '#A0522D' }; // EARTH - Sienna
case 'normal': return { symbol: '⚪', color: '#A9A9A9' }; // Generic - DarkGray
default: return { symbol: '❓', color: '#696969' }; // Default - DimGray
}
}
function onWindowResize() {
const arenaRect = battleArena.getBoundingClientRect();
if (arenaRect.width > 0 && arenaRect.height > 0 && renderer && camera) {
camera.aspect = arenaRect.width / arenaRect.height;
camera.updateProjectionMatrix();
renderer.setSize(arenaRect.width, arenaRect.height);
}
}
function get3DPositionFromSprite(spriteElement) {
// Ensure calculation runs only when element is visible and sized
if (!spriteElement || spriteElement.offsetParent === null || !camera) {
// console.warn("Sprite not ready for 3D positioning", spriteElement);
return new THREE.Vector3(0, 0, 0); // Return default if not ready
}
const spriteRect = spriteElement.getBoundingClientRect();
const arenaRect = battleArena.getBoundingClientRect();
const spriteCenterX = spriteRect.left - arenaRect.left + spriteRect.width / 2;
const spriteCenterY = spriteRect.top - arenaRect.top + spriteRect.height / 2; // Use bottom for origin maybe? Let's stick to center
const ndcX = (spriteCenterX / arenaRect.width) * 2 - 1;
const ndcY = -(spriteCenterY / arenaRect.height) * 2 + 1;
const vector = new THREE.Vector3(ndcX, ndcY, 0.5);
vector.unproject(camera);
const dir = vector.sub(camera.position).normalize();
const distance = -camera.position.z / dir.z;
const pos = camera.position.clone().add(dir.multiplyScalar(distance));
// Adjust Y slightly to match perceived sprite base
pos.y += (spriteElement === opponentSpriteEl) ? 0.2 : -0.3;
return pos;
}
// --- Particle Animation Logic ---
function createParticleEffect(config) {
const { count = 100, color = 0xffffff, size = 0.1, duration = 1.0, startPos, endPos, speed = 5, type = 'burst', stagger = 0.2 } = config;
if (!scene || !clock) return; // Safety check
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
const startTimes = new Float32Array(count);
const initialSize = new Float32Array(count); // For size variation
const baseTime = clock.getElapsedTime();
for (let i = 0; i < count; i++) {
positions[i * 3] = startPos.x + (Math.random() - 0.5) * 0.1; // Slight start variance
positions[i * 3 + 1] = startPos.y + (Math.random() - 0.5) * 0.1;
positions[i * 3 + 2] = startPos.z;
initialSize[i] = size * (0.8 + Math.random() * 0.4); // Size variation
if (type === 'burst') {
const phi = Math.random() * Math.PI * 2;
const costheta = Math.random() * 2 - 1;
const sintheta = Math.sqrt(1 - costheta * costheta);
velocities[i * 3] = sintheta * Math.cos(phi) * speed * (0.5 + Math.random());
velocities[i * 3 + 1] = sintheta * Math.sin(phi) * speed * (0.5 + Math.random());
velocities[i * 3 + 2] = costheta * speed * (0.5 + Math.random());
} else if (type === 'stream' && endPos) {
const dir = endPos.clone().sub(startPos).normalize();
const angleVariance = 0.2; // Radians spread
const randomAngleX = (Math.random() - 0.5) * angleVariance;
const randomAngleY = (Math.random() - 0.5) * angleVariance;
dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), randomAngleX); // Rotate slightly
dir.applyAxisAngle(new THREE.Vector3(1, 0, 0), randomAngleY);
velocities[i * 3] = dir.x * speed * (0.8 + Math.random() * 0.4);
velocities[i * 3 + 1] = dir.y * speed * (0.8 + Math.random() * 0.4);
velocities[i * 3 + 2] = dir.z * speed * (0.8 + Math.random() * 0.4);
} else { // Default burst
const phi = Math.random() * Math.PI * 2;
const costheta = Math.random() * 2 - 1;
const sintheta = Math.sqrt(1 - costheta * costheta);
velocities[i * 3] = sintheta * Math.cos(phi) * speed;
velocities[i * 3 + 1] = sintheta * Math.sin(phi) * speed;
velocities[i * 3 + 2] = costheta * speed;
}
startTimes[i] = baseTime + (Math.random() * stagger); // Stagger start
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
geometry.setAttribute('startTime', new THREE.BufferAttribute(startTimes, 1));
geometry.setAttribute('initialSize', new THREE.BufferAttribute(initialSize, 1)); // Store initial size
const material = new THREE.PointsMaterial({
color: color,
size: size, // Base size, might be overridden if shader used
transparent: true,
opacity: 0.9,
blending: THREE.AdditiveBlending,
sizeAttenuation: true,
depthWrite: false
});
const points = new THREE.Points(geometry, material);
points.userData = {
startTime: baseTime, // Overall system start time
duration: duration,
isParticleSystem: true,
needsUpdate: true, // Flag for the update loop
baseSize: size
};
scene.add(points);
activeParticles.push(points);
// console.log("Created particle effect:", type, "Color:", color); // Debug log
}
function updateParticles(deltaTime) {
if (!clock || !scene) return;
const currentTime = clock.getElapsedTime();
const particlesToRemove = [];
for (let i = activeParticles.length - 1; i >= 0; i--) {
const system = activeParticles[i];
// Handle Non-particle tracked objects (like bombs/rings)
if (!system.userData.isParticleSystem) {
if (system.update) { // Does it have a custom update function?
system.update(deltaTime);
}
// Check if its duration has passed
if (currentTime > system.userData.startTime + system.userData.duration) {
particlesToRemove.push(i);
scene.remove(system);
if(system.geometry) system.geometry.dispose();
if(system.material) system.material.dispose();
}
continue; // Move to next active particle/mesh
}
// Handle standard particle systems
const systemElapsedTime = currentTime - system.userData.startTime;
if (systemElapsedTime > system.userData.duration + 1.0) { // Add buffer for staggered particles
particlesToRemove.push(i);
scene.remove(system);
system.geometry.dispose();
system.material.dispose();
continue;
}
// Update individual particles within the system
const positions = system.geometry.attributes.position.array;
const velocities = system.geometry.attributes.velocity.array;
const startTimes = system.geometry.attributes.startTime.array;
const initialSizes = system.geometry.attributes.initialSize.array; // Get sizes
let aliveParticles = 0;
for (let j = 0; j < positions.length / 3; j++) {
const particleStartTime = startTimes[j];
if (currentTime >= particleStartTime) {
const particleElapsedTime = currentTime - particleStartTime;
// Check if individual particle life is over (relative to system duration)
if (particleElapsedTime < system.userData.duration) {
positions[j * 3] += velocities[j * 3] * deltaTime;
positions[j * 3 + 1] += velocities[j * 3 + 1] * deltaTime;
positions[j * 3 + 2] += velocities[j * 3 + 2] * deltaTime;
// Optional: Add gravity or forces here
// velocities[j*3+1] -= 9.8 * deltaTime * 0.1; // Simple weak gravity
aliveParticles++;
}
} else {
// Particle hasn't started yet, but is technically alive
aliveParticles++;
}
}
system.geometry.attributes.position.needsUpdate = true;
// Fade out the entire system based on its overall lifetime
const lifeLeft = 1.0 - Math.min(1.0, systemElapsedTime / system.userData.duration);
system.material.opacity = Math.max(0, lifeLeft * 0.9);
// system.material.size = system.userData.baseSize * lifeLeft; // Optional shrink
// If no particles will ever be alive again, mark for removal early
if (aliveParticles === 0 && systemElapsedTime > system.userData.duration) {
if (!particlesToRemove.includes(i)) { // Avoid duplicates
particlesToRemove.push(i);
scene.remove(system);
system.geometry.dispose();
system.material.dispose();
}
}
}
// Remove marked systems from the active list
particlesToRemove.sort((a, b) => b - a); // Sort descending to avoid index issues
particlesToRemove.forEach(index => activeParticles.splice(index, 1));
}
function animate() {
animationFrameId = requestAnimationFrame(animate);
if (!renderer || !scene || !camera || !clock) return; // Safety checks
const deltaTime = clock.getDelta();
updateParticles(deltaTime);
if (threeCanvas.offsetParent !== null) { // Render only if visible
renderer.render(scene, camera);
}
}
function startAnimationLoop() {
if (!animationFrameId) {
if(!clock) clock = new THREE.Clock(); // Ensure clock exists
clock.start();
animate();
// console.log("Animation loop started");
}
}
function stopAnimationLoop() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
if(clock) clock.stop();
// console.log("Animation loop stopped");
// Clean up particles immediately
activeParticles.forEach(system => {
scene.remove(system);
if(system.geometry) system.geometry.dispose();
if(system.material) system.material.dispose();
});
activeParticles = [];
}
}
// --- Core Game Logic (Modified) ---
function getRandomPokemon(excludeId = null) {
const availablePokemon = pokemonData.filter(p => p.id !== excludeId);
const randomIndex = Math.floor(Math.random() * availablePokemon.length);
return JSON.parse(JSON.stringify(availablePokemon[randomIndex]));
}
function updateHealthBar(pokemon, healthBarEl, hpTextEl) {
const hpPercent = Math.max(0, (pokemon.hp / pokemon.maxHp) * 100);
healthBarEl.style.width = `${hpPercent}%`;
hpTextEl.textContent = `HP: ${Math.max(0, pokemon.hp)} / ${pokemon.maxHp}`;
healthBarEl.className = 'health-bar'; // Reset classes first
if (hpPercent > 50) healthBarEl.classList.add('high');
else if (hpPercent > 20) healthBarEl.classList.add('medium');
else healthBarEl.classList.add('low');
}
function displayMessage(message, speed = 40) {
battleMessage.textContent = ''; // Clear first
let i = 0;
function typeWriter() {
if (i < message.length) {
battleMessage.textContent += message.charAt(i);
i++;
setTimeout(typeWriter, speed);
} else {
battleMessage.scrollTop = battleMessage.scrollHeight;
}
}
typeWriter();
}
function playCssAnimation(targetSprite, animationClass, duration = 400) {
targetSprite.classList.add(animationClass);
setTimeout(() => {
targetSprite.classList.remove(animationClass);
}, duration);
}
// Modified: Now just sets the flag, button state handled separately
function setAttackInProgress(inProgress) {
attackInProgress = inProgress;
// console.log("Attack In Progress:", attackInProgress); // Debug log
const buttons = attackOptionsContainer.querySelectorAll('.attack-button');
buttons.forEach(button => button.disabled = inProgress || !isPlayerTurn); // Also disable if not player turn
}
// --- Play 3D Attack Animations (REVISED Cases) ---
function play3DAttackAnimation(attack, attackerSprite, defenderSprite) {
if (!scene || !clock) return;
console.log(`Attempting 3D effect: ${attack.effectType} from ${attackerSprite.id} to ${defenderSprite.id}`);
// Get positions *just before* creating effect, ensuring elements are placed
const startPos = get3DPositionFromSprite(attackerSprite);
const endPos = get3DPositionFromSprite(defenderSprite);
// Ensure positions are valid (not default 0,0,0 if element wasn't ready)
if (startPos.lengthSq() === 0 || endPos.lengthSq() === 0) {
console.warn("Could not get valid 3D positions for animation.", startPos, endPos);
return; // Don't try to animate if positions are bad
}
switch (attack.effectType) {
case 'electric':
case 'electric_strong':
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream',
color: 0xFFFF00, size: attack.effectType === 'electric_strong' ? 0.12 : 0.09,
count: attack.effectType === 'electric_strong' ? 150 : 100,
speed: 12, duration: 0.7, stagger: 0.1
});
if (attack.effectType === 'electric_strong') {
setTimeout(() => createParticleEffect({
startPos: endPos, type: 'burst', color: 0xFFFF99, size: 0.25, count: 60, speed: 5, duration: 0.5
}), 450); // Impact burst delay
}
break;
case 'fire':
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream', color: 0xFF4500,
size: 0.18, count: 120, speed: 9, duration: 0.9, stagger: 0.2
});
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream', color: 0xFFD700,
size: 0.08, count: 70, speed: 9.5, duration: 0.9, stagger: 0.25
});
break;
case 'water':
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream', color: 0x1E90FF,
size: 0.15, count: 120, speed: 10, duration: 0.8, stagger: 0.15
});
break;
case 'water_bubble':
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream', color: 0xADD8E6,
size: 0.2, count: 50, speed: 7, duration: 1.1, stagger: 0.3
});
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream', color: 0xFFFFFF,
size: 0.08, count: 70, speed: 7.5, duration: 1.1, stagger: 0.35
});
break;
case 'grass_bomb':
case 'poison_bomb':
const bombColor = attack.effectType === 'grass_bomb' ? 0x00FF00 : 0x8A2BE2; // Green or BlueViolet
const bombGeo = new THREE.SphereGeometry(0.3, 16, 16);
const bombMat = new THREE.MeshBasicMaterial({ color: bombColor, transparent: true, opacity: 0.9 });
const bombMesh = new THREE.Mesh(bombGeo, bombMat);
bombMesh.position.copy(startPos);
bombMesh.userData = { isParticleSystem: false, startTime: clock.getElapsedTime(), duration: 0.6, start: startPos.clone(), end: endPos.clone() };
scene.add(bombMesh);
activeParticles.push(bombMesh);
bombMesh.update = function(deltaTime) { // Assign update directly
const progress = Math.min(1.0, (clock.getElapsedTime() - this.userData.startTime) / this.userData.duration);
this.position.lerpVectors(this.userData.start, this.userData.end, progress);
if (progress >= 1.0 && !this.userData.exploded) { // Explode only once
this.userData.exploded = true; // Prevent re-explosion
createParticleEffect({
startPos: this.position, type: 'burst', color: bombColor,
size: 0.2, count: 100, speed: 6, duration: 0.6
});
// Mesh removal is handled by the main update loop based on duration
}
};
break;
case 'grass_leaf':
for (let i=0; i<15; i++) {
setTimeout(() => {
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream', color: 0x228B22,
size: 0.12, count: 5, speed: 8 + (Math.random() * 2),
duration: 0.7 + (Math.random() * 0.2), stagger: 0.05
});
}, i * 35); // Stagger leaves
}
break;
case 'dark_ball':
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream', color: 0x483D8B,
size: 0.25, count: 60, speed: 8, duration: 0.9, stagger: 0.1
});
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream', color: 0x8A2BE2, // BlueViolet glow
size: 0.1, count: 60, speed: 8.5, duration: 0.9, stagger: 0.15
});
break;
case 'dark_pulse':
const pulseColor = 0x4B0082; // Indigo
for(let i = 0; i < 4; i++) { // More rings
setTimeout(() => {
const ringGeo = new THREE.RingGeometry(0.1 + i*0.6, 0.3 + i*0.6, 32);
const ringMat = new THREE.MeshBasicMaterial({ color: pulseColor, side: THREE.DoubleSide, transparent: true, opacity: 0.7 });
const ringMesh = new THREE.Mesh(ringGeo, ringMat);
ringMesh.position.copy(endPos);
ringMesh.position.y += 0.1; // Slightly lift pulse center
ringMesh.rotation.x = Math.PI / 1.8; // Angle slightly towards camera
ringMesh.userData = { isParticleSystem: false, startTime: clock.getElapsedTime(), duration: 0.6, startScale: 0.1, endScale: 1.0 + i*0.6 };
scene.add(ringMesh);
activeParticles.push(ringMesh);
ringMesh.update = function(deltaTime) {
const progress = Math.min(1.0, (clock.getElapsedTime() - this.userData.startTime) / this.userData.duration);
const easeOutQuad = t => t * (2 - t); // Easing function
const scale = THREE.MathUtils.lerp(this.userData.startScale, this.userData.endScale, easeOutQuad(progress));
this.scale.set(scale, scale, scale);
this.material.opacity = Math.max(0, 0.7 * (1.0 - progress));
};
}, i * 120); // Stagger rings
}
break;
case 'physical_dash':
// Attacker lunge (CSS/simple movement could be added) + quick particle trail
createParticleEffect({
startPos: startPos, endPos: endPos, type: 'stream', color: 0xFFFFFF,
size: 0.06, count: 40, speed: 18, duration: 0.35, stagger: 0.05
});
break;
case 'physical_impact':
// Impact burst at defender
createParticleEffect({
startPos: endPos, type: 'burst', color: 0xFFA500, // Orange sparks
size: 0.15, count: 60, speed: 5, duration: 0.5, stagger: 0.1
});
createParticleEffect({
startPos: endPos, type: 'burst', color: 0xFFFFFF, // White inner flash
size: 0.1, count: 40, speed: 6, duration: 0.4, stagger: 0.05
});
break;
default:
console.warn(`Unknown effectType: ${attack.effectType}. Playing default effect.`);
createParticleEffect({ // Default effect if type unknown
startPos: endPos, type: 'burst', color: 0xCCCCCC,
size: 0.1, count: 50, speed: 4, duration: 0.6
});
}
}
// --- handleAttack (REVISED Timing & State) ---
function handleAttack(attack, attacker, defender, attackerSprite, defenderSprite, defenderHealthBar, defenderHpText) {
// Check guards: Battle over, attack already in progress, or not the correct turn
if (isBattleOver || attackInProgress || (isPlayerTurn && attacker !== playerPokemon) || (!isPlayerTurn && attacker !== opponentPokemon)) {
// console.log("Attack prevented:", {isBattleOver, attackInProgress, isPlayerTurn, attacker: attacker.name});
return;
}
setAttackInProgress(true); // Set flag and disable buttons *immediately*
const message = `${attacker.name} used ${attack.name}!`;
displayMessage(message, 30);
// --- Define Timeouts ---
const PRE_ANIM_DELAY = 200; // Small pause before visual action
const POST_3D_START_DELAY = 100; // Time after 3D anim starts before attacker shake
const IMPACT_DELAY = 650; // Time from start until damage/hit flash (adjust based on typical 3D anim travel time)
const POST_IMPACT_MSG_DELAY = IMPACT_DELAY + 300; // Time until damage message update
const FAINT_CHECK_DELAY = POST_IMPACT_MSG_DELAY + 100; // Time until faint check
const TURN_SWITCH_DELAY = FAINT_CHECK_DELAY + 1200; // Total time before switching turn (allowing messages/faint anim)
// 1. Initial delay
setTimeout(() => {
// 2. Start 3D Animation
play3DAttackAnimation(attack, attackerSprite, defenderSprite);
// 3. Attacker animation (optional small shake) shortly after 3D starts
setTimeout(() => {
playCssAnimation(attackerSprite, 'sprite-shake-short', 300);
}, POST_3D_START_DELAY);
// 4. Damage application and defender hit animation at IMPACT_DELAY
setTimeout(() => {
defender.hp -= attack.damage;
updateHealthBar(defender, defenderHealthBar, defenderHpText);
playCssAnimation(defenderSprite, 'sprite-hit-flash', 400); // CSS flash on hit
// 5. Update battle message with damage info after impact
setTimeout(() => {
const damageMessage = ` It dealt ${attack.damage} damage.`;
displayMessage(message + damageMessage, 30);
// 6. Check for faint after damage message is displayed
setTimeout(() => {
if (defender.hp <= 0) {
// Faint sequence handles its own delays and game over state
handleFaint(defender, defenderSprite);
// Faint handles setting battle over and showing overlay, no turn switch needed here.
} else {
// 7. If no faint, schedule the turn switch
setTimeout(switchTurn, TURN_SWITCH_DELAY - FAINT_CHECK_DELAY); // Remaining time until turn switch
}
}, FAINT_CHECK_DELAY - POST_IMPACT_MSG_DELAY); // Delay relative to message update
}, POST_IMPACT_MSG_DELAY - IMPACT_DELAY); // Delay relative to impact
}, IMPACT_DELAY - PRE_ANIM_DELAY); // Adjust delay relative to outer timeout
}, PRE_ANIM_DELAY);
}
function handleFaint(faintedPokemon, faintedSprite) {
// No need to check attackInProgress here, as it's part of the attack sequence
isBattleOver = true; // Set battle over flag *immediately*
playCssAnimation(faintedSprite, 'sprite-faint', 1000);
const winner = (faintedPokemon === opponentPokemon) ? playerPokemon : opponentPokemon;
const loser = faintedPokemon;
const message = `${loser.name} fainted!`;
displayMessage(message); // Show faint message first
setTimeout(() => { // Delay win message and game over screen
displayMessage(`${message} ${winner.name} wins!`);
showGameOver(winner === playerPokemon);
// Don't set attackInProgress = false here, resetGame handles it
}, 1200); // Allow faint animation to play out mostly
}
function switchTurn() {
if(isBattleOver) return; // Don't switch turns if game has ended
isPlayerTurn = !isPlayerTurn;
// console.log("Switching turn. Player turn:", isPlayerTurn);
if (!isPlayerTurn) {
// Opponent's turn
displayMessage(`Opponent ${opponentPokemon.name}'s turn...`);
// Buttons remain disabled (set by setAttackInProgress(true) earlier)
setTimeout(opponentAttack, 1500 + Math.random() * 500);
} else {
// Player's turn - *Crucially*, re-enable controls and reset flag
displayMessage(`Your turn! Choose an attack.`);
setAttackInProgress(false); // Reset flag and enable buttons
}
}
function opponentAttack() {
if(isBattleOver) return; // Check again before opponent attacks
const randomAttackIndex = Math.floor(Math.random() * opponentPokemon.attacks.length);
const attack = opponentPokemon.attacks[randomAttackIndex];
// Opponent's handleAttack call will set attackInProgress back to true
handleAttack(attack, opponentPokemon, playerPokemon, opponentSpriteEl, playerSpriteEl, playerHealthBarEl, playerHpTextEl);
}
function setupBattle(selectedPlayerPokemon) {
playerPokemon = JSON.parse(JSON.stringify(selectedPlayerPokemon));
opponentPokemon = getRandomPokemon(playerPokemon.id);
isBattleOver = false;
isPlayerTurn = true; // Player always starts
attackInProgress = false; // Ensure reset at start
stopAnimationLoop(); // Clear any old effects
if (!scene) { // Initialize Three.js only if needed
initThree();
} else { // Clear previous non-light objects if scene exists
activeParticles.forEach(system => { // Clear array first
scene.remove(system);
if(system.geometry) system.geometry.dispose();
if(system.material) system.material.dispose();
});
activeParticles = [];
// Also remove any other temporary meshes if needed
const objectsToRemove = scene.children.filter(child =>
!(child instanceof THREE.Light) && !(child instanceof THREE.Camera)
);
objectsToRemove.forEach(obj => scene.remove(obj));
}
// Update Player UI
playerNameEl.textContent = playerPokemon.name;
playerSpriteEl.src = playerPokemon.spriteUrl;
playerSpriteEl.alt = playerPokemon.name;
playerSpriteEl.className = 'pokemon-sprite'; // Reset classes
playerSpriteEl.style.opacity = 1;
updateHealthBar(playerPokemon, playerHealthBarEl, playerHpTextEl);
// Update Opponent UI
opponentNameEl.textContent = opponentPokemon.name;
opponentSpriteEl.src = opponentPokemon.spriteUrl;
opponentSpriteEl.alt = opponentPokemon.name;
opponentSpriteEl.className = 'pokemon-sprite'; // Reset classes
opponentSpriteEl.style.opacity = 1;
updateHealthBar(opponentPokemon, opponentHealthBarEl, opponentHpTextEl);
// Create Attack Buttons
attackOptionsContainer.innerHTML = '';
playerPokemon.attacks.forEach(attack => {
const button = document.createElement('button');
button.classList.add('attack-button');
button.textContent = attack.name;
button.onclick = () => handleAttack(attack, playerPokemon, opponentPokemon, playerSpriteEl, opponentSpriteEl, opponentHealthBarEl, opponentHpTextEl);
attackOptionsContainer.appendChild(button);
});
displayMessage(`Battle start! ${playerPokemon.name} vs ${opponentPokemon.name}. Your turn!`);
setAttackInProgress(false); // Explicitly enable buttons for player start
gameContainer.classList.add('battle-active');
setTimeout(() => {
onWindowResize(); // Ensure canvas size is correct after transition
startAnimationLoop();
// Force redraw/reflow to help positioning? (Usually not needed)
// battleArena.offsetHeight;
}, 650); // Slightly longer delay to ensure layout settled
}
function showGameOver(playerWon) {
gameOverMessage.textContent = playerWon ? "You Won!" : "You Lost!";
gameOverOverlay.classList.add('visible');
stopAnimationLoop();
}
function resetGame() {
playerPokemon = null;
opponentPokemon = null;
isPlayerTurn = true;
isBattleOver = false;
attackInProgress = false;
stopAnimationLoop(); // Stop 3D loop
// Reset UI elements
playerNameEl.textContent = "Player";
playerHpTextEl.textContent = "HP: ??? / ???";
playerHealthBarEl.style.width = '100%';
playerHealthBarEl.className = 'health-bar high';
playerSpriteEl.src = "";
playerSpriteEl.style.opacity = 0;
opponentNameEl.textContent = "Opponent";
opponentHpTextEl.textContent = "HP: ??? / ???";
opponentHealthBarEl.style.width = '100%';
opponentHealthBarEl.className = 'health-bar high';
opponentSpriteEl.src = "";
opponentSpriteEl.style.opacity = 0;
attackOptionsContainer.innerHTML = '';
battleMessage.textContent = 'Select a Pokémon to start the battle!';
gameOverOverlay.classList.remove('visible');
gameContainer.classList.remove('battle-active');
setTimeout(initializeSelection, 600);
}
function initializeSelection() {
pokemonCardsContainer.innerHTML = ''; // Clear existing cards
pokemonData.forEach(pokemon => {
const card = document.createElement('div');
// Add base class and type-specific class for styling hooks
const typeClass = `type-${pokemon.type.toLowerCase()}`;
card.classList.add('pokemon-card', typeClass);
card.innerHTML = `
<div class="card-image-container">
<img src="${pokemon.spriteUrl}" alt="${pokemon.name}" class="card-image">
</div>
<div class="card-content">
<h3 class="card-name">${pokemon.name}</h3>
<div class="card-info">
<span class="card-type-chip">${pokemon.type}</span>
<span class="card-hp">HP: ${pokemon.maxHp}</span>
</div>
${pokemon.description ? `<p class="card-description">${pokemon.description}</p>` : ''}
</div>
<div class="card-ripple"></div>
`; // Added ripple element
card.onclick = (e) => {
if (!gameContainer.classList.contains('battle-active') && !attackInProgress) {
// --- Ripple Effect ---
const ripple = card.querySelector('.card-ripple');
const rect = card.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = `${size}px`;
ripple.style.left = `${x}px`;
ripple.style.top = `${y}px`;
ripple.classList.add('active');
// Remove ripple effect after animation
setTimeout(() => ripple.classList.remove('active'), 600);
// --- Proceed to setup battle after short delay ---
setTimeout(() => setupBattle(pokemon), 150); // Slight delay for ripple visibility
}
};
pokemonCardsContainer.appendChild(card);
});
}
// --- Event Listeners ---
playAgainButton.addEventListener('click', resetGame);
// --- Initial Load ---
document.addEventListener('DOMContentLoaded', () => {
initializeSelection();
opponentSpriteEl.style.opacity = 0; // Hide sprites initially
playerSpriteEl.style.opacity = 0;
});
</script>
</body>
</html>