<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>WIN Bird Flap</title>
<link href="https://fonts.googleapis.com/css2?family=Strait&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/tone@14.7.71/build/Tone.js"></script>
<style>
/* * FIX: ENCAPSULACIÓN DE ESTILOS PARA EVITAR INTERFERENCIA CON ELEMENTOR
* Todos los estilos globales (como html, body, *) han sido eliminados.
* El estilo ahora solo afecta al contenedor #game-container y sus hijos.
*/
/* 1. Reset básico y fuente, SCOPED al contenedor principal */
#game-container {
/* Fuente y color que antes estaban en 'body' */
font-family: 'Strait', sans-serif;
color: white;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Propiedades de posicionamiento y tamaño */
position: relative;
margin: 0 auto;
margin-top: 5vh;
/* Forzamos una relación de aspecto (ej. 9:16) */
aspect-ratio: 9 / 16;
height: 90vh;
max-height: 750px;
width: auto;
max-width: 95vw;
/* Fallback para navegadores que no soportan aspect-ratio */
@supports not (aspect-ratio: 9 / 16) {
width: calc(90vh * 9 / 16);
max-width: 95vw;
}
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
overflow: hidden;
background: linear-gradient(to bottom, #87CEEB 0%, #ADD8E6 100%); /* Cielo */
/* Asegura que los hijos usen border-box */
box-sizing: border-box;
}
/* 2. Aplicar box-sizing al contenido del juego */
#game-container *, #game-container *::before, #game-container *::after {
box-sizing: border-box;
/* También aplicamos el reseteo de márgenes/paddings internamente, si fuera necesario
aunque con el posicionamiento absoluto de las UI, esto es menos crítico. */
margin: 0;
padding: 0;
}
/* El lienzo del juego */
#game-canvas {
width: 100%;
height: 100%;
display: block;
}
/* Superposiciones de UI (Puntaje, Inicio, Fin) */
.ui-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
padding: 20px;
}
/* Mensaje de inicio */
#start-message {
background: rgba(0, 0, 0, 0.3);
font-size: clamp(1.2rem, 4vw, 1.8rem);
z-index: 10;
}
#start-message h1 {
font-size: clamp(2rem, 8vw, 3rem);
}
/* Pantalla de Game Over */
#game-over-screen {
background: rgba(0, 0, 0, 0.7);
font-size: clamp(1.5rem, 5vw, 2.2rem);
z-index: 10;
display: none;
}
#game-over-screen h2 {
/* Título "JUEGO TERMINADO" - Reducido a 5vw / 2rem máx. */
font-size: clamp(1.5rem, 5vw, 2rem);
margin-bottom: 10px;
}
#final-score {
font-size: clamp(1rem, 3.5vw, 1.5rem);
margin-bottom: 20px;
}
#final-score strong {
font-size: clamp(1.5rem, 5.5vw, 2.2rem);
color: #FFD700;
display: block;
margin-top: 10px;
}
/* Ocultar el botón de Reiniciar */
#restart-button {
display: none;
}
/* Pantalla de puntaje en el juego */
#score-display {
position: absolute;
top: 5%;
left: 50%;
transform: translateX(-50%);
font-size: clamp(2.5rem, 10vw, 3.5rem);
font-weight: bold;
z-index: 5;
display: none;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="score-display">0</div>
<div id="start-message" class="ui-overlay">
<h1>WIN Bird Flap</h1>
<p style="margin-top: 20px; font-size: clamp(0.9rem, 3vw, 1.1rem);">Modo Clásico: ¡Esquiva los tubos!</p>
<p style="font-size: clamp(0.9rem, 3vw, 1.1rem);">Toca o haz clic para empezar</p>
<p style="font-size: clamp(0.6rem, 2vw, 0.9rem); margin-top: 5px;">(El audio comenzará con la primera interacción)</p>
</div>
<div id="game-over-screen" class="ui-overlay">
<h2>¡JUEGO TERMINADO!</h2>
<p id="final-score">Puntaje Final: <strong>0</strong></p>
<button id="restart-button">Jugar de Nuevo</button>
</div>
</div>
<script>
// Inicializamos el contexto de audio en la primera interacción para evitar errores
let audioContextStarted = false;
// --- 1. CONFIGURACIÓN DE AUDIO (TONE.JS) ---
const musicVolume = new Tone.Volume(-15).toDestination();
const reverb = new Tone.Reverb({ decay: 4, wet: 0.5 }).connect(musicVolume);
const synth = new Tone.PolySynth(Tone.Synth, {
oscillator: { type: "sine" },
envelope: { attack: 2, decay: 0.5, sustain: 0.5, release: 3 }
}).connect(reverb);
const chords = [
["C4", "E4", "G4", "B4"],
["A3", "C4", "E4", "G4"],
["F3", "A3", "C4", "E4"],
["G3", "B3", "D4", "F4"]
];
let chordIndex = 0;
const musicLoop = new Tone.Loop(time => {
const chord = chords[chordIndex % chords.length];
synth.triggerAttackRelease(chord, "2n", time);
chordIndex++;
}, "2n");
// Sonido de aleteo (rápido y percusivo para evitar errores de temporización)
const flapSound = new Tone.Synth({
volume: -15,
oscillator: { type: "square" },
envelope: {
attack: 0.005,
decay: 0.05,
sustain: 0,
release: 0.05,
}
}).toDestination();
function playFlapSound() {
flapSound.triggerAttackRelease("C6", "32n");
}
const scoreSound = new Tone.DuoSynth({
vibratoAmount: 0.5, vibratoRate: 5, harmonicity: 1.5,
voice0: { volume: -10, oscillator: { type: "square" }, envelope: { attack: 0.005, decay: 0.2, sustain: 0.4, release: 0.8, } },
voice1: { volume: -10, oscillator: { type: "sine" }, envelope: { attack: 0.005, decay: 0.2, sustain: 0.4, release: 0.8, } }
}).toDestination();
function playScoreSound() {
scoreSound.triggerAttackRelease("C6", "8n");
}
// Sonido de Crash/Colisión (MetalSynth con Distorsión)
const distortion = new Tone.Distortion(0.8).toDestination();
const crashSynth = new Tone.MetalSynth({
volume: -10,
frequency: 100,
envelope: {
attack: 0.001,
decay: 0.2,
release: 0.1
},
harmonicity: 5.1,
modulationIndex: 32,
resonance: 4000,
octaves: 1.5
}).connect(distortion);
function playCrashSound() {
crashSynth.triggerAttackRelease("C4", "16n", Tone.now(), 0.5);
}
// --- 2. CONFIGURACIÓN INICIAL (Resto del código) ---
// Elementos del DOM
const gameContainer = document.getElementById('game-container');
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const startMessage = document.getElementById('start-message');
const gameOverScreen = document.getElementById('game-over-screen');
const scoreDisplay = document.getElementById('score-display');
const finalScoreDisplay = document.getElementById('final-score');
// const restartButton = document.getElementById('restart-button'); // No usado al estar oculto
// Definimos una resolución lógica fija para el juego
const LOGICAL_WIDTH = 320;
const LOGICAL_HEIGHT = 568;
canvas.width = LOGICAL_WIDTH;
canvas.height = LOGICAL_HEIGHT;
// --- 3. VARIABLES DEL JUEGO ---
// Constantes de física y juego
const BIRD_WIDTH = 28;
const BIRD_HEIGHT = 22;
const GRAVITY = 0.30;
const FLAP_STRENGTH = -6.0;
const PIPE_WIDTH = 50;
// Constantes para la dificultad
const INITIAL_PIPE_GAP = 140;
const INITIAL_PIPE_SPEED = 1.8;
const INITIAL_PIPE_SPAWN_INTERVAL = 120;
// Parámetros de dificultad progresiva
const DIFFICULTY_START_TIME = 40 * 60; // Dificultad aumenta por los primeros 40 segundos de juego
const MIN_PIPE_GAP = 110;
const MAX_PIPE_SPEED = 2.5;
// Variables de estado del juego
let currentPipeGap;
let currentPipeSpeed;
let bird, pipes, score, frame, gameState;
// --- 4. CLASES Y OBJETOS DEL JUEGO ---
// Objeto Pájaro
class Bird {
constructor() {
this.x = 50;
this.y = LOGICAL_HEIGHT / 2 - BIRD_HEIGHT / 2;
this.width = BIRD_WIDTH;
this.height = BIRD_HEIGHT;
this.velocityY = 0;
this.rotation = 0;
}
// Actualiza la física del pájaro
update() {
this.velocityY += GRAVITY;
this.y += this.velocityY;
// Calcula la rotación
if (this.velocityY < 0) {
this.rotation = -0.3;
} else {
this.rotation = Math.min(this.velocityY * 0.08, Math.PI / 3);
}
// Colisión con el suelo (TERMINA EL JUEGO)
const GROUND_HEIGHT = 40;
if (this.y + this.height > LOGICAL_HEIGHT - GROUND_HEIGHT) {
this.y = LOGICAL_HEIGHT - GROUND_HEIGHT - this.height;
this.velocityY = 0;
finalEndGame(); // Llama a la función de fin de juego (Game Over)
return;
}
// Colisión con el cielo
if (this.y < 0) {
this.y = 0;
this.velocityY = 0;
}
}
// Dibuja el pájaro
draw() {
ctx.save();
ctx.translate(this.x + this.width / 2, this.y + this.height / 2);
ctx.rotate(this.rotation);
// Cuerpo
ctx.fillStyle = '#000080'; // Cambiado a azul marino
ctx.beginPath();
ctx.ellipse(0, 0, this.width / 2, this.height / 2, 0, 0, 2 * Math.PI);
ctx.fill();
// Ala
ctx.fillStyle = '#4169E1'; // Cambiado a un azul más claro (Royal Blue) para contraste
ctx.beginPath();
ctx.ellipse(-3, 2, this.width / 4, this.height / 4, 0.3, 0, 2 * Math.PI);
ctx.fill();
// Ojo
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(this.width / 4, -this.height / 6, 4, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(this.width / 4 + 1, -this.height / 6, 2, 0, 2 * Math.PI);
ctx.fill();
// Pico
ctx.fillStyle = 'orange';
ctx.beginPath();
ctx.moveTo(this.width / 2, -2);
ctx.lineTo(this.width / 2 + 8, this.height / 6 - 2);
ctx.lineTo(this.width / 2, this.height / 3 - 2);
ctx.fill();
ctx.restore();
}
// Impulso hacia arriba
flap() {
this.velocityY = FLAP_STRENGTH;
if (gameState === 'playing') {
playFlapSound();
}
}
}
// --- 5. FUNCIONES PRINCIPALES DEL JUEGO ---
// Función para actualizar la dificultad basándose en el tiempo de la ronda
function updateDifficulty() {
// El aumento de dificultad se basa en el número de frames jugados (frame)
const progress = Math.min(1, frame / DIFFICULTY_START_TIME);
currentPipeGap = INITIAL_PIPE_GAP - (INITIAL_PIPE_GAP - MIN_PIPE_GAP) * progress;
currentPipeSpeed = INITIAL_PIPE_SPEED + (MAX_PIPE_SPEED - INITIAL_PIPE_SPEED) * progress;
}
// Función para limpiar el estado del juego (Bird, Pipes, Score)
function resetGameState() {
bird = new Bird();
pipes = [];
score = 0; // Puntaje de la partida actual
frame = 0; // Frames de la partida actual
scoreDisplay.textContent = '0';
}
// Inicializa o resetea el juego completo
function resetGame() {
// Reset de estados
currentPipeGap = INITIAL_PIPE_GAP;
currentPipeSpeed = INITIAL_PIPE_SPEED;
resetGameState(); // Prepara el estado inicial del juego
gameState = 'start';
// Detiene la música si ya estaba corriendo
Tone.Transport.stop();
musicLoop.stop();
chordIndex = 0;
// Configuración de la UI
startMessage.style.display = 'flex';
gameOverScreen.style.display = 'none';
scoreDisplay.style.display = 'none';
}
// Bucle principal del juego
function gameLoop() {
if (gameState !== 'over') {
update();
}
draw();
requestAnimationFrame(gameLoop);
}
// Función de actualización de estado
function update() {
if (gameState !== 'playing') return;
frame++; // Frame counter for the current game
updateDifficulty();
bird.update();
updatePipes();
checkCollisions();
}
// Función de dibujado en el lienzo
function draw() {
ctx.clearRect(0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT);
drawPipes();
drawGround();
bird.draw();
}
// Comienza la partida (llamado en el primer click)
function startGame() {
if (gameState === 'start') {
resetGameState();
// Iniciar contexto de audio y música si no se ha hecho
if (!audioContextStarted) {
Tone.start();
Tone.Transport.bpm.value = 60;
audioContextStarted = true;
}
Tone.Transport.start();
musicLoop.start(0);
gameState = 'playing';
startMessage.style.display = 'none';
scoreDisplay.style.display = 'block';
bird.flap(); // Primer impulso
}
}
// Termina el juego COMPLETAMENTE (solo por colisión)
function finalEndGame() {
if (gameState === 'over') return; // Evita ejecuciones múltiples
gameState = 'over';
playCrashSound();
// Parar la música
Tone.Transport.stop();
musicLoop.stop();
// Mostrar la pantalla de Game Over y el puntaje
gameOverScreen.style.display = 'flex';
// Texto actualizado a JUEGO TERMINADO
gameOverScreen.querySelector('h2').textContent = "¡JUEGO TERMINADO!";
finalScoreDisplay.innerHTML = `Puntaje Final: <strong>${score}</strong>`;
scoreDisplay.style.display = 'none';
// --- INICIO DE LA MODIFICACIÓN ---
// Enviamos el puntaje a la página contenedora (Elementor)
if (window.parent && window.parent !== window) {
// Usamos un objeto para poder identificar el mensaje
window.parent.postMessage({
type: 'winBirdFlapScore', // Identificador único
score: score // El puntaje final
}, '*'); // En producción, deberías cambiar '*' por el dominio de tu web (ej: "https://tu-web.com")
}
// --- FIN DE LA MODIFICACIÓN ---
// NOTA: El botón de reiniciar está oculto por CSS.
}
// --- 6. LÓGICA DE OBSTÁCULOS (TUBERÍAS) ---
function updatePipes() {
// Añadir nueva tubería (el intervalo de aparición es constante)
if (frame % INITIAL_PIPE_SPAWN_INTERVAL === 0) {
const GROUND_HEIGHT = 40;
let topHeight = Math.random() * (LOGICAL_HEIGHT - currentPipeGap - GROUND_HEIGHT - 80) + 40;
let bottomHeight = LOGICAL_HEIGHT - topHeight - currentPipeGap - GROUND_HEIGHT;
pipes.push({
x: LOGICAL_WIDTH,
topHeight: topHeight,
bottomHeight: bottomHeight,
scored: false
});
}
// Mover y eliminar tuberías
for (let i = pipes.length - 1; i >= 0; i--) {
let p = pipes[i];
p.x -= currentPipeSpeed; // Movimiento hacia la izquierda
if (p.x + PIPE_WIDTH < 0) {
pipes.splice(i, 1);
}
// Comprobar puntuación
if (!p.scored && p.x + PIPE_WIDTH < bird.x) {
p.scored = true;
score++;
scoreDisplay.textContent = score.toString();
playScoreSound();
}
}
}
function drawPipes() {
ctx.fillStyle = '#28a745';
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
for (const p of pipes) {
// Tubería de arriba
ctx.fillRect(p.x, 0, PIPE_WIDTH, p.topHeight);
ctx.fillRect(p.x - 5, p.topHeight - 20, PIPE_WIDTH + 10, 20);
// Tubería de abajo
const GROUND_HEIGHT = 40;
ctx.fillRect(p.x, LOGICAL_HEIGHT - GROUND_HEIGHT - p.bottomHeight, PIPE_WIDTH, p.bottomHeight);
ctx.fillRect(p.x - 5, LOGICAL_HEIGHT - GROUND_HEIGHT - p.bottomHeight, PIPE_WIDTH + 10, 20);
// Bordes (opcional para estilo)
ctx.strokeRect(p.x, 0, PIPE_WIDTH, p.topHeight);
ctx.strokeRect(p.x - 5, p.topHeight - 20, PIPE_WIDTH + 10, 20);
ctx.strokeRect(p.x, LOGICAL_HEIGHT - GROUND_HEIGHT - p.bottomHeight, PIPE_WIDTH, p.bottomHeight);
ctx.strokeRect(p.x - 5, LOGICAL_HEIGHT - GROUND_HEIGHT - p.bottomHeight, PIPE_WIDTH + 10, 20);
}
}
// --- 7. LÓGICA DE COLISIONES Y SUELO ---
function checkCollisions() {
const GROUND_HEIGHT = 40;
for (const p of pipes) {
// Comprobación de colisión AABB
if (
bird.x < p.x + PIPE_WIDTH &&
bird.x + bird.width > p.x &&
(bird.y < p.topHeight || bird.y + bird.height > LOGICAL_HEIGHT - GROUND_HEIGHT - p.bottomHeight)
) {
finalEndGame(); // Colisión con tubería: TERMINA EL JUEGO
return;
}
}
}
function drawGround() {
const GROUND_HEIGHT = 40;
ctx.fillStyle = '#D2B48C';
ctx.fillRect(0, LOGICAL_HEIGHT - GROUND_HEIGHT, LOGICAL_WIDTH, GROUND_HEIGHT);
// Textura de hierba
ctx.fillStyle = '#228B22';
ctx.fillRect(0, LOGICAL_HEIGHT - GROUND_HEIGHT, LOGICAL_WIDTH, 10);
// Líneas de detalle (Movimiento hacia la IZQUIERDA)
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
ctx.lineWidth = 1;
// Desplazamiento basado en el frame y velocidad, asegurando que sea hacia la izquierda (-)
const scrollSpeed = currentPipeSpeed * 0.5;
const offset = (-frame * scrollSpeed) % LOGICAL_WIDTH;
for(let i = 0; i < LOGICAL_WIDTH / 40 + 2; i++) {
const x1 = (i * 40 + offset) % LOGICAL_WIDTH;
const x2 = ((i * 40 + 10) + offset) % LOGICAL_WIDTH;
// Asegurar que las coordenadas no sean negativas y se envuelvan correctamente
const getWrappedX = (val) => (val < 0) ? val + LOGICAL_WIDTH : val;
ctx.beginPath();
ctx.moveTo( getWrappedX(x1), LOGICAL_HEIGHT - GROUND_HEIGHT + 15);
ctx.lineTo( getWrappedX(x2), LOGICAL_HEIGHT - GROUND_HEIGHT / 2);
ctx.stroke();
}
}
// --- 8. MANEJADORES DE EVENTOS (INPUT) ---
function handleInput(e) {
e.preventDefault();
if (gameState === 'start') {
startGame();
} else if (gameState === 'playing') {
bird.flap();
}
}
// Eventos para móvil y escritorio
gameContainer.addEventListener('mousedown', handleInput);
gameContainer.addEventListener('touchstart', handleInput, { passive: false });
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
handleInput(e);
}
});
// El manejador del botón de reiniciar se ha eliminado al estar el botón oculto.
// --- 9. INICIO DEL JUEGO ---
// Iniciar el juego al cargar la página
resetGame();
gameLoop();
</script>
</body>
</html>