function draw() {
// Handle Level 100 event first
if (level === 100) {
// Draw sky background
background(135, 206, 235); // Sky blue
// Draw the airplane
drawAirplane(width / 2, youY - 20); // Airplane follows player's Y slightly
// Draw ground (grass) once player starts falling
if (playerFalling || playerInPool || playerHitGround) {
noStroke();
fill(34, 139, 34); // Forest green for grass
rect(0, height * 0.7, width, height * 0.3); // Grass covers the bottom 30% of the screen
// Draw the pool
fill(0, 191, 255); // Deep blue for pool
rect(poolX - poolWidth / 2, poolY - poolHeight / 2, poolWidth, poolHeight, 10);
// Pool border
noFill();
stroke(200);
strokeWeight(5);
rect(poolX - poolWidth / 2, poolY - poolHeight / 2, poolWidth, poolHeight, 10);
}
// Update player position if falling
if (playerFalling) {
fallVelocity += GRAVITY;
youY += fallVelocity;
// Allow horizontal movement with mouse/touch while falling
youX = mouseX;
if (touches.length > 0) {
youX = touches[0].x;
}
youX = constrain(youX, 20, width - 20); // Constrain player X
// Check collision with pool
if (
youY + 10 > poolY - poolHeight / 2 && // Bottom of player hits top of pool
youY - 10 < poolY + poolHeight / 2 && // Top of player is above bottom of pool
youX + 20 > poolX - poolWidth / 2 && // Right of player hits left of pool
youX - 20 < poolX + poolWidth / 2 // Left of player hits right of pool
) {
playerFalling = false;
playerInPool = true;
// Play splash sound
if (splashSound) {
splashSound.amp(0.5, 0.1);
splashSound.freq(400, 0.1);
setTimeout(() => splashSound.amp(0, 0.5), 500);
}
}
// Check collision with ground (if not in pool)
else if (youY + 10 > height * 0.7) {
playerFalling = false;
playerHitGround = true;
// Play splat sound
if (splatSound) {
splatSound.amp(0.8, 0.1);
splatSound.freq(100, 0.1);
setTimeout(() => splatSound.amp(0, 0.5), 500);
}
}
}
// Draw "You" character
fill(100);
noStroke();
ellipse(youX, youY, 40, 20); // Use youY
fill(0);
textSize(16);
textAlign(CENTER, CENTER); // Center "You" text
text("You", youX, youY + 10); // Use youY
// Display messages for Level 100
fill(0);
textSize(24);
textAlign(CENTER, TOP);
if (!playerFalling && !playerInPool && !playerHitGround) {
text("Level 100: The Impossible Airplane!", width / 2, 20);
text("Fire hit the plane! Tap 'Jump!' to escape and find the pool below!", width / 2, 60);
} else if (playerInPool) {
textSize(48);
text("You survived!", width / 2, height / 2 - 50);
textSize(24);
text("The impossible apple is safe! Moving to the next level.", width / 2, height / 2 + 10);
nextLevel(); // Advance to Level 101 immediately
} else if (playerHitGround) {
textSize(48);
text("Game Over!", width / 2, height / 2 - 50);
textSize(24);
text("You hit the ground. The impossible apple was defeated.", width / 2, height / 2 + 10);
noLoop();
restartButton.show();
}
return; // Stop drawing for this frame to prevent other level logic from running
}
// 3. Draw the sky and grass to set the scene. (Only if not Level 100)
background(135, 206, 235); // Sky blue
// Display current level
fill(0);
textSize(20);
textAlign(LEFT, TOP);
text(`Level: ${level}/${MAX_LEVELS}`, 20, 20);
// Apply the panning translation to the scene elements
push(); // Save the current transformation matrix
translate(translateX, translateY);
// New: Draw location-specific background elements BEFORE grass and apple
switch (currentLocationType) {
case 'SUN':
drawSun();
break;
case 'MOON':
drawMoon();
break;
case 'CLOUD':
drawClouds();
break;
case 'SPACE':
drawStars();
break;
case 'HOUSE': // NEW: Draw the house
// The house is drawn at its center, so we need to pass a center coordinate
let houseCenterX = (width * 0.2 + width * 0.8) / 2; // Approximate center of canvas
let houseCenterY = height * 0.65; // Just above the grass line
drawHouse(houseCenterX, houseCenterY, 120, 80);
break;
}
noStroke();
fill(34, 139, 34); // Forest green for grass
rect(0, height * 0.7, width, height * 0.3); // Grass covers the bottom 30% of the screen
// Handle Level 10 event
if (level === 10) {
if (foodPoliceActive) {
drawFoodPoliceCar(foodPoliceX, tinyAppleY);
foodPoliceX -= FOOD_POLICE_SPEED; // Move left
// Check if food police car has reached the apple
if (foodPoliceX < tinyAppleX) {
appleArrested = true; // Mark apple as arrested (will make it disappear)
foodPoliceActive = false; // Food police car disappears
jailActive = true; // Jail appears
jailX = tinyAppleX; // Position jail at apple's location
jailY = tinyAppleY;
jailCarryingApple = true; // Apple is inside jail
foundApple = true; // Set foundApple to true to trigger win message and character leaving
}
}
if (jailActive) {
drawJail(jailX, jailY, jailCarryingApple);
jailY += FOOD_POLICE_SPEED; // Jail sinks down
// Check if jail has gone off-screen
if (jailY > height + 50) {
jailActive = false;
jailCarryingApple = false;
// At this point, foundApple is already true, and 'You' is leaving.
// We now explicitly call nextLevel() for Level 10 here.
nextLevel(); // Explicitly advance to the next level for Level 10
// We can also return here to prevent any further drawing from the old level in this frame.
return;
}
}
}
// NEW: Handle Level 20 Zombie event
else if (level === 20) {
if (level20ZombieEventTriggered) { // Zombie event is active
if (zombieActive) {
// Move and draw zombie
zombie.move();
zombie.draw();
// Apple shooting logic
if (millis() - lastShotTime > 500) { // Shoot every 500ms
bullets.push(new Bullet(tinyAppleX, tinyAppleY, zombie.x, zombie.y - zombie.size * 0.6)); // Target zombie head
lastShotTime = millis();
// Play apple shooting sound
if (appleShootingSound) {
appleShootingSound.amp(0.2, 0.05);
appleShootingSound.freq(1000, 0.05);
setTimeout(() => appleShootingSound.amp(0, 0.1), 100);
}
}
// Move and draw bullets
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].move();
bullets[i].draw();
if (!bullets[i].isAlive) {
bullets.splice(i, 1);
}
}
// Check bullet-zombie collision
for (let i = bullets.length - 1; i >= 0; i--) {
let b = bullets[i];
if (b.isAlive && zombie.isAlive) {
let d = dist(b.x, b.y, zombie.x, zombie.y - zombie.size * 0.6); // Target zombie head
if (d < zombie.size * 0.4) {
zombie.takeDamage();
b.isAlive = false; // Bullet disappears on hit
bullets.splice(i, 1);
if (!zombie.isAlive) {
zombieDefeated = true;
zombieActive = false;
}
}
}
}
// Check zombie-apple collision (zombie reaches apple)
let d = dist(zombie.x, zombie.y - zombie.size * 0.6, tinyAppleX, tinyAppleY);
if (d < currentAppleSize / 2 + zombie.size * 0.4) { // Zombie touches apple
appleEaten = true;
zombieActive = false;
// Play apple eaten sound or game over music
if (zombieHitSound) { // Reusing zombie hit sound for apple eaten for now
zombieHitSound.amp(0.5, 0.1);
zombieHitSound.freq(50, 0.1);
setTimeout(() => zombieHitSound.amp(0, 0.5), 1000);
}
}
} else if (zombieDefeated) {
// Win Level 20
textSize(48);
textAlign(CENTER, TOP); // Ensure text is centered
text("You defeated the zombie!", width / 2, height / 2 - 50);
textSize(24);
text("The apple is safe! Moving to the next level.", width / 2, height / 2 + 10);
nextLevel(); // Advance to Level 21 immediately
return; // Stop drawing for this frame to prevent flickering
} else if (appleEaten) {
// Lose Level 20
textSize(48);
textAlign(CENTER, TOP); // Ensure text is centered
text("The zombie ate the apple!", width / 2, height / 2 - 50);
textSize(24);
text("Game Over. The impossible apple was defeated.", width / 2, height / 2 + 10);
// Stop the draw loop
noLoop();
// You could add a button to restart here
restartButton.show();
}
} else { // Zombie event not triggered yet on Level 20
text("You are standing on the grass, looking out.", width / 2, 20);
text("Somewhere on the horizon, 10,000,000 feet away, is a single apple.", width / 2, 60);
text("It's so far, it would appear as a speck smaller than a pixel to the naked eye.", width / 2, 100);
text("It's impossible to find. But wait... you hear a groan?", width / 2, 140);
text("Can you spot it?", width / 2, 180);
}
}
// NEW: Handle Level 30 Fruit Chase event
else if (level === 30) {
if (level30EventTriggered) {
// Hide hint button when chase starts
hintButton.hide(); // Moved this inside the triggered block
// Removed: Hide fast leave button during chase
// fastLeaveButton.hide();
// Draw the exit door
if (exitDoor) exitDoor.draw();
// Update player movement: directly follow mouse/touch
youX = mouseX; // Or touches[0].x if touchActive
youY = mouseY; // Make youY dynamic for Level 30
if (touches.length > 0) {
youX = touches[0].x;
youY = touches[0].y;
}
youX = constrain(youX, 20, width - 20); // Constrain player X
youY = constrain(youY, 20, height - 20); // Constrain player Y to a reasonable range
// The "You" character will be drawn later using youX, youY
// If fruit is active, move and draw it, check for collision
if (fruitActive && fruit) {
fruit.targetX = youX; // Fruit targets the player
fruit.targetY = youY; // Fruit targets the player's Y position
fruit.move();
fruit.draw();
// Collision detection: Fruit vs Player
let d = dist(fruit.x, fruit.y, youX, youY); // Use youY
if (d < fruit.size / 2 + 20) { // 20 is approx half of "You" character's ellipse width
fruitCaughtPlayer = true;
fruitActive = false; // Fruit stops chasing
// Play player caught sound
if (playerCaucaught) {
playerCaucaught.amp(0.5, 0.1);
playerCaucaught.freq(map(random(), 0, 1, 200, 300), 0.1); // Gulp/scream sound
setTimeout(() => playerCaucaught.amp(0, 0.5), 1000);
}
}
}
// Collision detection: Player vs Exit Door
if (exitDoor && !playerEscaped) {
// playerY is now youY
if (
youX > exitDoor.x - exitDoor.width / 2 &&
youX < exitDoor.x + exitDoor.width / 2 &&
youY > exitDoor.y - exitDoor.height / 2 && // Use youY
youY < exitDoor.y + exitDoor.height / 2 // Use youY
) {
playerEscaped = true;
fruitActive = false; // Fruit stops chasing
// Play door open sound
if (doorOpenSound) {
doorOpenSound.amp(0.4, 0.1);
doorOpenSound.freq(map(random(), 0, 1, 500, 700), 0.1); // Whoosh/creak sound
setTimeout(() => doorOpenSound.amp(0, 0.5), 500);
}
}
}
// Win/Lose conditions for Level 30
if (playerEscaped) {
textSize(48);
textAlign(CENTER, TOP);
text("You escaped the hungry fruit!", width / 2, height / 2 - 50);
textSize(24);
text("The impossible apple is safe! Moving to the next level.", width / 2, height / 2 + 10);
nextLevel(); // Advance to Level 31
return; // Stop drawing for this frame
} else if (fruitCaughtPlayer) {
textSize(48);
textAlign(CENTER, TOP);
text("The fruit ate you!", width / 2, height / 2 - 50);
textSize(24);
text("Game Over. The impossible apple was defeated.", width / 2, height / 2 + 10);
noLoop(); // Stop the draw loop
restartButton.show(); // Show the restart button
return; // Stop drawing for this frame
}
// Display instructions during the chase
fill(0);
textSize(24);
textAlign(CENTER, TOP);
text("ESCAPE THE HUNGRY FRUIT!", width / 2, 20);
text("Find the EXIT door!", width / 2, 60);
} else { // Level 30, but event not triggered yet
// Hint button remains visible here to help find the apple
text("You are standing on the grass, looking out.", width / 2, 20);
text("Somewhere on the horizon, 10,000,000 feet away, is a single apple.", width / 2, 60);
text("It's so far, it would appear as a speck smaller than a pixel to the naked eye.", width / 2, 100);
text("It's impossible to find. But wait... is that a fruit with eyes?", width / 2, 140);
text("Can you spot it?", width / 2, 180);
}
}
// 4. Draw the tiny, impossible apple.
// The apple is now only drawn if it hasn't been found yet AND we haven't completed the game
// On Level 10, the apple disappears when arrested (appleArrested = true)
// On Level 20, the apple disappears if eaten by the zombie (appleEaten = true)
// On Level 30, the apple disappears when the fruit chase event starts (level30EventTriggered = true)
if (!foundApple && level <= MAX_LEVELS && !(level === 10 && appleArrested) && !(level === 20 && appleEaten) && !cakeEventActive && !level30EventTriggered) {
image(appleBuffer, tinyAppleX, tinyAppleY, currentAppleSize, currentAppleSize);
}
// Draw hint circle if active
if (hintActive) {
noFill();
stroke(255, 0, 0); // Red circle for hint
strokeWeight(3);
// Draw the circle around the apple's untranslated position
ellipse(tinyAppleX, tinyAppleY, currentHintCircleSize, currentHintCircleSize);
// Make the hint disappear after the currentHintDuration
if (millis() - hintTimer > currentHintDuration) {
hintActive = false;
}
}
// Optional: Add a simple hint for your position
// Only draw "You" if the apple hasn't been found yet for regular levels,
// AND it's not a special event level where "You" might be handled differently or hidden
// NEW: Also hide "You" during the cake event, and level 30 event
if (!foundApple && level <= MAX_LEVELS && !cakeEventActive && !level30EventTriggered && level !== 100) {
fill(100);
noStroke();
ellipse(youX, youY, 40, 20); // Use youY
fill(0);
textSize(16);
textAlign(CENTER, CENTER); // Center "You" text
text("You", youX, youY + 10); // Use youY
}
pop(); // Restore the transformation matrix, so text explanation is drawn fixed on screen
// 5. Add text explanation to convey the concept.
fill(0); // Black text
textSize(24);
textAlign(CENTER, TOP);
if (level > MAX_LEVELS && !cakeEventActive) { // Final win message after cake event is complete
textSize(48);
text("YOU WON THE GAME!", width / 2, height / 2 - 50);
textSize(24);
text("Thanks for playing! You found all impossible apples and had some cake fun!", width / 2, height / 2 + 10);
noLoop(); // Stop the game loop
restartButton.show(); // Show the restart button
} else if (foundApple && level !== 10 && level !== 20 && level !== 30) { // This block handles win for normal levels
// NEW LOGIC: Immediately display win message and advance to the next level
textSize(48); // Make "You win!" bigger
text("You win!", width / 2, height / 2 - 50);
textSize(24);
text("You ate the impossible apple and left!", width / 2, height / 2 + 10);
nextLevel();
// The `nextLevel()` function will reset `foundApple` to `false` and set up the new level.
// We can `return` here to prevent any further drawing from the old level in this frame.
return;
}
// The Level 10, Level 20, Level 30, and Level 100 specific text is handled within their respective blocks above.
// NEW: Handle Level 1000 Cake event
if (cakeEventActive) {
hintButton.hide(); // Hide hint button during cake event
// Removed: Hide fast leave button during cake event
// fastLeaveButton.hide();
restartButton.hide(); // Hide restart button if it was shown from Level 20 loss
// Draw animals
for (let animal of targetAnimals) {
animal.move();
animal.draw();
}
// Draw and move thrown cakes
for (let i = thrownCakes.length - 1; i >= 0; i--) {
thrownCakes[i].move();
thrownCakes[i].draw();
if (!thrownCakes[i].isActive) {
thrownCakes.splice(i, 1);
}
}
// Check collisions between cakes and animals
for (let i = thrownCakes.length - 1; i >= 0; i--) {
let cake = thrownCakes[i];
if (!cake.isActive) continue;
for (let j = targetAnimals.length - 1; j >= 0; j--) {
let animal = targetAnimals[j];
if (animal.isHit) continue; // Skip already hit animals
let d = dist(cake.x, cake.y, animal.x, animal.y);
if (d < cake.size / 2 + animal.size / 2) {
// Collision!
animal.isHit = true;
animal.splatTimer = millis();
cake.isActive = false; // Cake disappears
// Play cake splat sound
if (cakeSplatSound) {
cakeSplatSound.amp(0.6, 0.1);
cakeSplatSound.freq(map(random(), 0, 1, 100, 200), 0.1);
setTimeout(() => cakeSplatSound.amp(0, 0.5), 500);
}
// Play animal ouch sound
if (animalOuchSound) {
animalOuchSound.amp(0.4, 0.05);
animalOuchSound.freq(map(random(), 0, 1, 400, 600), 0.05);
setTimeout(() => animalOuchSound.amp(0, 0.2), 200);
}
thrownCakes.splice(i, 1); // Remove cake
// No need to remove animal yet, it will animate and disappear later
break; // Only one animal can be hit per cake
}
}
}
// Draw the cake if player is holding one
if (hasCake) {
push();
translate(mouseX, mouseY);
noStroke();
// Cake body (brown cake)
fill(139, 69, 19);
rect(-15, -15, 30, 15, 5);
// Icing (white)
fill(255);
ellipse(0, -15, 30, 10);
// Cherry on top (red)
fill(200, 0, 0);
ellipse(0, -15 - 5, 10, 10);
pop();
}
// Display instructions
fill(0);
textSize(32);
textAlign(CENTER, TOP);
text("Congratulations! You found all impossible apples!", width / 2, 20);
textSize(24);
if (hasCake) {
text("Tap to throw the cake at a dog, cat, or bunny for fun!", width / 2, 60);
} else {
text("More cakes are coming!", width / 2, 60);
// Automatically give another cake after a delay if all thrown cakes are gone
if (thrownCakes.length === 0 && targetAnimals.some(a => !a.isHit)) {
setTimeout(() => { hasCake = true; }, 1000); // Give another cake after 1 second
}
}
// Check if all animals are hit
if (targetAnimals.every(a => a.isHit)) {
cakeEventActive = false; // End cake event
level = MAX_LEVELS + 1; // Set level higher than MAX_LEVELS to trigger final win message
// The final win message will be displayed in the next draw cycle
}
}
}