<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Wow Crossy Road - Three.js</title>
    <!-- Link External CSS -->
    <link rel="stylesheet" href="style.css">
    <!-- Google Font -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@700&display=swap" rel="stylesheet">
</head>
<body>
    <!-- UI Elements -->
    <div id="ui-container">
        <div id="info">
            Score: <span id="score">0</span>
            <br> <!-- New line for coins -->
            Coins: <span id="coins">0</span>
        </div>
        <div id="game-over" style="display: none;"> <!-- Use style initially -->
            <h2>Game Over!</h2>
            <p>Final Score: <span id="final-score">0</span></p>
            <p>Total Coins: <span id="final-coins">0</span></p>
            <button id="restart-button">Restart</button>
        </div>
    </div>

    <!-- Three.js canvas will be added here by script -->

    <!-- Import Three.js -->
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
            }
        }
    </script>
    <script type="module" src="game.js"></script>
</body>
</html>
body {
    margin: 0;
    overflow: hidden;
    background-color: #51a7f9; /* Lighter sky blue */
    font-family: 'Nunito', sans-serif; /* Apply custom font */
    color: #fff; /* Default text color */
}

canvas {
    display: block;
}

#ui-container {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none; /* Allow clicks to pass through to the canvas */
    display: flex;
    flex-direction: column;
    align-items: center; /* Center game over screen */
}

#info {
    position: absolute;
    top: 15px;
    left: 15px;
    font-size: 1.5em; /* Larger score */
    background-color: rgba(0, 40, 80, 0.6); /* Darker, semi-transparent */
    padding: 10px 15px;
    border-radius: 8px;
    line-height: 1.4; /* Spacing for score/coins */
    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
    pointer-events: auto; /* Allow interaction if needed later */
    z-index: 10; /* Above canvas */
}

#game-over {
    margin-top: 25vh; /* Position lower than exact center */
    background-color: rgba(255, 255, 255, 0.9);
    color: #333; /* Dark text on light background */
    padding: 30px 40px;
    border-radius: 15px;
    text-align: center;
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
    pointer-events: auto; /* Allow button clicks */
    z-index: 10;
}

#game-over h2 {
    color: #e74c3c; /* Reddish color for heading */
    margin-bottom: 15px;
    font-size: 2.5em;
}

#game-over p {
    font-size: 1.2em;
    margin: 10px 0;
}

#game-over button {
    font-family: 'Nunito', sans-serif;
    font-size: 1.1em;
    padding: 12px 25px;
    margin-top: 20px;
    cursor: pointer;
    background-color: #2ecc71; /* Green */
    color: white;
    border: none;
    border-radius: 8px;
    transition: background-color 0.2s ease, transform 0.1s ease;
    box-shadow: 0 3px 5px rgba(0,0,0,0.2);
}

#game-over button:hover {
    background-color: #27ae60; /* Darker green */
}

#game-over button:active {
    transform: translateY(1px);
    box-shadow: 0 2px 3px rgba(0,0,0,0.2);
}
import * as THREE from "https://esm.sh/three";

// --- Basic Setup ---
const scene = new THREE.Scene();
const FOG_COLOR = 0x6ab0de; // Slightly desaturated blue
scene.background = new THREE.Color(FOG_COLOR); // Match fog color
scene.fog = new THREE.Fog(FOG_COLOR, 10, 25); // Add fog for depth

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Softer shadows
document.body.appendChild(renderer.domElement);

// --- Camera ---
const aspectRatio = window.innerWidth / window.innerHeight;
const cameraWidth = 15;
const cameraHeight = cameraWidth / aspectRatio;
const camera = new THREE.OrthographicCamera(
    cameraWidth / -2, cameraWidth / 2, cameraHeight / 2, cameraHeight / -2, 0.1, 100
);
camera.position.set(4, 5, 5); // Slightly higher angle
camera.lookAt(0, 0, 0);

// --- Lighting ---
// Use HemisphereLight for softer ambient light
const hemisphereLight = new THREE.HemisphereLight(0xB1E1FF, 0xB97A20, 0.6); // Sky, Ground, Intensity
scene.add(hemisphereLight);

const sunLight = new THREE.DirectionalLight(0xffffff, 0.8); // Main sun light
sunLight.position.set(8, 12, 8); // Adjust position
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048; // Higher res shadows
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 0.5;
sunLight.shadow.camera.far = 50;
sunLight.shadow.camera.left = -cameraWidth; // Match camera view bounds roughly
sunLight.shadow.camera.right = cameraWidth;
sunLight.shadow.camera.top = cameraHeight * 2; // Allow shadows from higher up
sunLight.shadow.camera.bottom = -cameraHeight * 2;
sunLight.shadow.bias = -0.001; // Adjust to prevent shadow artifacts
scene.add(sunLight);
scene.add(sunLight.target);

// --- Texture Loading ---
const textureLoader = new THREE.TextureLoader();
const textures = {
    grass: textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/terrain/grasslight-big.jpg'),
    road: textureLoader.load('https://cdn.jsdelivrnet.gh/mrdoob/three.js/examples/textures/terrain/brick_roughness.jpg'), // Placeholder road
    water: textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/waternormals.jpg'),
    log: textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/hardwood2_diffuse.jpg'),
    chicken: textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/crate.gif'), // Placeholder chicken
    car_side: textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/brick_diffuse.jpg') // Placeholder car
};
// Texture settings
for (const key in textures) {
    textures[key].wrapS = THREE.RepeatWrapping;
    textures[key].wrapT = THREE.RepeatWrapping;
    if (key === 'grass') {
        textures[key].repeat.set(3, 3); // Repeat grass texture more
    }
     if (key === 'road') {
        textures[key].repeat.set(5, 1); // Repeat road texture horizontally
    }
     if (key === 'log') {
        textures[key].repeat.set(1, 0.5);
    }
    if (key === 'water') {
        textures[key].repeat.set(2, 2); // Animate water later maybe
    }
}

// --- Materials (Using textures) ---
const materials = {
    grass: new THREE.MeshLambertMaterial({ map: textures.grass }),
    road: new THREE.MeshLambertMaterial({ map: textures.road, color: 0x555555 }), // Darken road base color
    water: new THREE.MeshLambertMaterial({ map: textures.water, opacity: 0.8, transparent: true, color: 0x55aaff }), // Make water transparent
    log: new THREE.MeshLambertMaterial({ map: textures.log }),
    chicken: new THREE.MeshLambertMaterial({ map: textures.chicken }),
    coin: new THREE.MeshStandardMaterial({ color: 0xffd700, metalness: 0.5, roughness: 0.6 }), // Shiny gold
    car: [ // Array for multi-material cars
        new THREE.MeshLambertMaterial({ map: textures.car_side, color: 0xdddddd }), // Sides (placeholder)
        new THREE.MeshLambertMaterial({ map: textures.car_side, color: 0xcccccc }), // Top/Bottom (placeholder)
        new THREE.MeshLambertMaterial({ color: 0x333333 }), // Wheels (dark grey)
    ],
    carColors: [0xff4444, 0x4444ff, 0x44ff44, 0xffff44, 0xff44ff] // Base colors to tint car materials
};

// --- Constants ---
const TILE_SIZE = 1;
const LANE_WIDTH = 15;
const MOVE_DURATION = 0.15;
const OBSTACLE_BASE_SPEED = 2.5; // Slightly faster base speed
const LANE_TYPES = ['grass', 'road', 'water'];
const COIN_CHANCE = 0.2; // Chance for a coin to spawn on a grass tile

// --- Game State ---
let score = 0;
let coins = 0; // Coin counter
let currentLaneIndex = 0;
let playerX = 0;
let isMoving = false;
let moveTimer = 0;
let isGameOver = false;
let activeLanes = {};
let activeCoins = {}; // { 'x_z': coinMesh }
let playerStartPos = new THREE.Vector3();
let playerTargetPos = new THREE.Vector3();
let cameraTargetPos = new THREE.Vector3();
const clock = new THREE.Clock();
let playerAttachedLog = null;
let lastLogSpeed = 0;

// --- DOM Elements ---
const scoreElement = document.getElementById('score');
const coinsElement = document.getElementById('coins'); // Coin UI element
const gameOverElement = document.getElementById('game-over');
const restartButton = document.getElementById('restart-button');
const finalScoreElement = document.getElementById('final-score');
const finalCoinsElement = document.getElementById('final-coins');

// --- Player (Chicken - Slightly more shape) ---
const chickenBodyGeo = new THREE.BoxGeometry(TILE_SIZE * 0.5, TILE_SIZE * 0.5, TILE_SIZE * 0.5);
const chickenHeadGeo = new THREE.BoxGeometry(TILE_SIZE * 0.25, TILE_SIZE * 0.25, TILE_SIZE * 0.25);
const chickenBeakGeo = new THREE.ConeGeometry(TILE_SIZE * 0.1, TILE_SIZE * 0.15, 4);

const chickenBody = new THREE.Mesh(chickenBodyGeo, materials.chicken);
const chickenHead = new THREE.Mesh(chickenHeadGeo, materials.chicken);
const chickenBeak = new THREE.Mesh(chickenBeakGeo, new THREE.MeshLambertMaterial({color: 0xffaa00}));

chickenBody.position.y = TILE_SIZE * 0.25; // Center body part
chickenHead.position.y = TILE_SIZE * 0.55; // Head above body
chickenHead.position.z = -TILE_SIZE * 0.1; // Head slightly forward
chickenBeak.position.y = TILE_SIZE * 0.55;
chickenBeak.position.z = -TILE_SIZE * 0.25; // Beak forward
chickenBeak.rotation.x = Math.PI / 2;

const chicken = new THREE.Group(); // Use a Group to hold chicken parts
chicken.add(chickenBody);
chicken.add(chickenHead);
chicken.add(chickenBeak);
chicken.position.y = TILE_SIZE * 0.25; // Base of the chicken group sits on the ground
chicken.castShadow = true; // Group doesn't cast shadow, apply to meshes if needed individually later
chickenBody.castShadow = true;
chickenHead.castShadow = true;

playerTargetPos.copy(chicken.position);
playerStartPos.copy(chicken.position);
scene.add(chicken);


// --- World Generation ---
const visibleLanes = 18; // See a bit further
const initialLanes = 6;

function generateInitialWorld() {
    // Start with more grass
    for (let i = -initialLanes; i <= initialLanes; i++) {
        createLane(i, i <= 1 ? 'grass' : getRandomLaneType()); // More initial grass
    }
    updateCameraTarget();
    camera.position.z = cameraTargetPos.z;
    camera.position.x = cameraTargetPos.x;
    camera.position.y = cameraTargetPos.y + 1; // Start slightly higher
}

function getRandomLaneType() {
    return LANE_TYPES[Math.floor(Math.random() * LANE_TYPES.length)];
}

function createLane(index, type = getRandomLaneType()) {
    if (activeLanes[index]) return;

    const positionZ = index * TILE_SIZE;
    let laneMaterial;
    if (type === 'grass') laneMaterial = materials.grass;
    else if (type === 'road') laneMaterial = materials.road;
    else if (type === 'water') laneMaterial = materials.water;
    else laneMaterial = materials.grass; // Default fallback

    const laneGeometry = new THREE.BoxGeometry(LANE_WIDTH, TILE_SIZE * 0.2, TILE_SIZE);
    const laneMesh = new THREE.Mesh(laneGeometry, laneMaterial);
    laneMesh.position.set(0, -TILE_SIZE * 0.1, positionZ);
    laneMesh.receiveShadow = true;
    scene.add(laneMesh);

    const laneData = { type, mesh: laneMesh, obstacles: [] };
    activeLanes[index] = laneData;

    const direction = Math.random() < 0.5 ? 1 : -1;
    const speedVariation = (Math.random() * 0.6 + 0.7); // 0.7x to 1.3x base speed
    const speed = OBSTACLE_BASE_SPEED * speedVariation * direction;

    if (type === 'road') {
        const numCars = Math.floor(Math.random() * 3) + 1;
        for (let i = 0; i < numCars; i++) {
            createCar(laneData, positionZ, direction, speed);
        }
    } else if (type === 'water') {
        const numLogs = Math.floor(Math.random() * 2) + 2;
        for (let i = 0; i < numLogs; i++) {
            createLog(laneData, positionZ, direction, speed);
        }
    } else if (type === 'grass') {
        // Maybe spawn coins?
        for(let x = -Math.floor(LANE_WIDTH/2) + 1; x < Math.floor(LANE_WIDTH/2); x++) {
            if (Math.random() < COIN_CHANCE) {
                createCoin(x * TILE_SIZE, positionZ);
            }
        }
    }
    return laneData;
}

function createCar(laneData, positionZ, direction, speed) {
    // Simple car shape using multiple boxes
    const carBodyWidth = TILE_SIZE * (Math.random() * 1.0 + 1.0); // 1 to 2
    const carBodyHeight = TILE_SIZE * 0.4;
    const carBodyDepth = TILE_SIZE * 0.8;
    const wheelRadius = TILE_SIZE * 0.15;
    const wheelDepth = TILE_SIZE * 0.1;

    // Clone base materials and apply random color tint
    const bodyMaterial = materials.car[0].clone();
    const topMaterial = materials.car[1].clone();
    bodyMaterial.color.set(materials.carColors[Math.floor(Math.random() * materials.carColors.length)]);
    topMaterial.color.copy(bodyMaterial.color).multiplyScalar(0.8); // Slightly darker top

    const car = new THREE.Group();
    const body = new THREE.Mesh(new THREE.BoxGeometry(carBodyWidth, carBodyHeight, carBodyDepth), bodyMaterial);
    body.position.y = wheelRadius + carBodyHeight / 2; // Body above wheels
    body.castShadow = true;
    car.add(body);

    // Add simple wheels (cylinders)
    const wheelGeo = new THREE.CylinderGeometry(wheelRadius, wheelRadius, wheelDepth, 16);
    const wheelMat = materials.car[2]; // Dark wheel material
    const wheelPositions = [
        { x: carBodyWidth / 2 - wheelRadius*1.2, y: wheelRadius, z: carBodyDepth / 2 - wheelRadius },
        { x: -carBodyWidth / 2 + wheelRadius*1.2, y: wheelRadius, z: carBodyDepth / 2 - wheelRadius },
        { x: carBodyWidth / 2 - wheelRadius*1.2, y: wheelRadius, z: -carBodyDepth / 2 + wheelRadius },
        { x: -carBodyWidth / 2 + wheelRadius*1.2, y: wheelRadius, z: -carBodyDepth / 2 + wheelRadius },
    ];
    wheelPositions.forEach(pos => {
        const wheel = new THREE.Mesh(wheelGeo, wheelMat);
        wheel.rotation.z = Math.PI / 2; // Orient cylinder horizontally
        wheel.position.set(pos.x, pos.y, pos.z);
        wheel.castShadow = true;
        car.add(wheel);
    });

    const startOffset = (laneData.obstacles.length * (LANE_WIDTH / 2.5)) % LANE_WIDTH;
    const initialX = (direction > 0 ? -LANE_WIDTH / 1.5 : LANE_WIDTH / 1.5) + startOffset;
    car.position.set(initialX, 0, positionZ); // Base of the group at y=0

    const obstacleData = { mesh: car, type: 'car', speed, direction, width: carBodyWidth };
    laneData.obstacles.push(obstacleData);
    scene.add(car);
}

function createLog(laneData, positionZ, direction, speed) {
    // Rounded box for log shape
    const logWidth = TILE_SIZE * (Math.random() * 2.5 + 2.0); // 2.0 to 4.5
    const logHeight = TILE_SIZE * 0.18;
    const logDepth = TILE_SIZE * 0.9;

    // Use BoxGeometry and apply texture
    const logGeometry = new THREE.BoxGeometry(logWidth, logHeight, logDepth);
    const log = new THREE.Mesh(logGeometry, materials.log);
    log.castShadow = true;
    log.receiveShadow = true;

    const startOffset = (laneData.obstacles.length * (LANE_WIDTH / 2.0)) % LANE_WIDTH;
    const initialX = (direction > 0 ? -LANE_WIDTH / 1.5 : LANE_WIDTH / 1.5) + startOffset;
    // Position log slightly lower so player lands *on* it visually
    log.position.set(initialX, logHeight / 2 - TILE_SIZE * 0.12, positionZ);

    const obstacleData = { mesh: log, type: 'log', speed, direction, width: logWidth };
    laneData.obstacles.push(obstacleData);
    scene.add(log);
}

function createCoin(positionX, positionZ) {
    const coinSize = TILE_SIZE * 0.3;
    const coinThickness = TILE_SIZE * 0.08;
    const coinGeometry = new THREE.CylinderGeometry(coinSize, coinSize, coinThickness, 16);
    const coin = new THREE.Mesh(coinGeometry, materials.coin);

    coin.position.set(positionX, coinSize + coinThickness, positionZ); // Position above ground
    coin.rotation.x = Math.PI / 2; // Lay flat initially
    coin.castShadow = true;

    const key = `${positionX}_${positionZ}`;
    activeCoins[key] = coin;
    scene.add(coin);
}

function manageLanes() {
    const playerLogicZ = Math.round(chicken.position.z / TILE_SIZE);

    const forwardLaneIndex = playerLogicZ + Math.floor(visibleLanes / 2);
    if (!activeLanes[forwardLaneIndex]) {
        createLane(forwardLaneIndex);
    }

    const backwardLaneIndex = playerLogicZ - Math.ceil(visibleLanes / 2);
    if (activeLanes[backwardLaneIndex]) {
        const laneToRemove = activeLanes[backwardLaneIndex];
        if (laneToRemove) {
             laneToRemove.obstacles.forEach(obstacle => scene.remove(obstacle.mesh));
             scene.remove(laneToRemove.mesh);
             delete activeLanes[backwardLaneIndex];

             // Also remove coins from that lane
             for (const key in activeCoins) {
                 if (Math.round(activeCoins[key].position.z / TILE_SIZE) === backwardLaneIndex) {
                     scene.remove(activeCoins[key]);
                     delete activeCoins[key];
                 }
             }
        }
    }
}

// --- Movement & Input (handleKeyDown remains similar) ---
document.addEventListener('keydown', handleKeyDown);

function handleKeyDown(event) {
    if (isMoving || isGameOver) return;

    let moveDirection = null;
    let targetX = playerX;
    let targetZIndex = currentLaneIndex;

    switch (event.key) {
        case 'ArrowUp': case 'w': targetZIndex++; moveDirection = 'forward'; break;
        case 'ArrowDown': case 's': if (currentLaneIndex > -initialLanes + 1) { targetZIndex--; moveDirection = 'backward'; } break;
        case 'ArrowLeft': case 'a': targetX--; moveDirection = 'left'; break;
        case 'ArrowRight': case 'd': targetX++; moveDirection = 'right'; break;
    }

    if (moveDirection) {
        if(playerAttachedLog) {
            // Detach player visually slightly before jump
            chicken.position.y = TILE_SIZE * 0.25;
            lastLogSpeed = 0;
            playerAttachedLog = null;
        }
      
              // --- continue handleKeyDown ---
        playerStartPos.copy(chicken.position);
        // Adjust target Y based on whether landing on water (aim for log height)
        const targetLane = activeLanes[targetZIndex];
        const targetY = (targetLane && targetLane.type === 'water') ? (TILE_SIZE * 0.1) : (TILE_SIZE * 0.25); // Lower target Y for water lanes
        playerTargetPos.set(targetX * TILE_SIZE, targetY, targetZIndex * TILE_SIZE);


        isMoving = true;
        moveTimer = 0;

        // Update logical positions immediately
        if (moveDirection === 'forward' && targetZIndex > currentLaneIndex) {
            score++;
            scoreElement.textContent = score;
        }
        currentLaneIndex = targetZIndex;
        playerX = targetX;

        manageLanes();
        updateCameraTarget();
    }
}


// --- Update Functions ---

function updatePlayerPosition(delta) {
    // --- Handle Log Riding ---
    if (!isMoving && playerAttachedLog && !isGameOver) {
        // Apply log speed from last frame
        const logDeltaX = lastLogSpeed * delta;
        chicken.position.x += logDeltaX;
        // Ensure player doesn't slide off the sides of the log
        const logBox = new THREE.Box3().setFromObject(playerAttachedLog.mesh);
        chicken.position.x = Math.max(logBox.min.x + TILE_SIZE * 0.2, Math.min(logBox.max.x - TILE_SIZE * 0.2, chicken.position.x));

        playerStartPos.copy(chicken.position); // Update start for potential next jump
        playerTargetPos.copy(chicken.position); // Keep target aligned while riding

        // Keep player visually ON the log
        chicken.position.y = playerAttachedLog.mesh.position.y + TILE_SIZE * 0.25 + (TILE_SIZE * 0.18 / 2); // log center Y + chicken height offset + half log height

    } else if (!isMoving && !isGameOver) {
         // If not moving and not on log, ensure player is at standard ground height
         chicken.position.y = TILE_SIZE * 0.25;
    }

    // Update lastLogSpeed for next frame AFTER applying current movement
    if (playerAttachedLog) {
        lastLogSpeed = playerAttachedLog.speed;
    } else {
        lastLogSpeed = 0;
    }


    // --- Handle Jumping Animation ---
    if (!isMoving) return;

    moveTimer += delta;
    let t = Math.min(moveTimer / MOVE_DURATION, 1.0);

    // Ease out cubic easing function: 1 - Math.pow(1 - t, 3)
    const easedT = 1 - Math.pow(1 - t, 3);

    chicken.position.lerpVectors(playerStartPos, playerTargetPos, easedT);

    // More pronounced hop animation
    const hopHeight = TILE_SIZE * 0.4;
    chicken.position.y = playerStartPos.y + Math.sin(t * Math.PI) * hopHeight;
     // Ensure hop respects target Y for water landings
     if (t > 0.5) { // On the way down
         const landingY = playerTargetPos.y; // Target Y already adjusted for water/ground
         const peakY = playerStartPos.y + hopHeight;
         const descentT = (t - 0.5) * 2; // Normalize descent phase (0 to 1)
         chicken.position.y = peakY + (landingY - peakY) * (1 - Math.pow(1 - descentT, 2)); // Ease in descent
     }


    // --- Movement Finished ---
    if (t >= 1.0) {
        isMoving = false;
        moveTimer = 0;
        chicken.position.y = playerTargetPos.y; // Snap to final target Y

        // Check for coin collection AT destination
        checkCoinCollection();
        // Final check for landing situation (water, road hazards)
        checkLanding();
    }
}


function updateCameraTarget() {
    // Camera follows player's X, stays offset Z and Y
    cameraTargetPos.x = chicken.position.x; // Follow X more directly for smoother feel
    cameraTargetPos.z = chicken.position.z + 5; // Keep camera Z offset from player Z
    cameraTargetPos.y = 5; // Keep camera height fixed
}


function updateCameraPosition(delta) {
    // Smoother camera lerp
    const lerpFactor = 1.0 - Math.exp(-delta * 4.0); // Adjust multiplier for speed (4.0 is smoother)
    camera.position.lerp(cameraTargetPos, lerpFactor);
    // Look slightly down towards where the player is going
    const lookAtPos = chicken.position.clone();
    lookAtPos.y -= 1.0; // Look lower than the chicken's feet
    camera.lookAt(lookAtPos);
}


function updateObstacles(delta) {
    const wrapMargin = TILE_SIZE * 3; // Increase margin for larger/faster objects
    const screenBounds = {
        left: -LANE_WIDTH / 2 - wrapMargin,
        right: LANE_WIDTH / 2 + wrapMargin
    };

    // Animate Coins
    for (const key in activeCoins) {
        activeCoins[key].rotation.z += delta * 2; // Rotate coin around its cylinder axis
        activeCoins[key].position.y = TILE_SIZE * 0.4 + Math.sin(clock.elapsedTime * 3 + activeCoins[key].position.x) * 0.1; // Bobbing effect
    }

    // Animate Obstacles
    for (const laneIndex in activeLanes) {
        const lane = activeLanes[laneIndex];
        lane.obstacles.forEach(obstacle => {
            obstacle.mesh.position.x += obstacle.speed * delta;

            // Rotate car wheels if it's a car
            if (obstacle.type === 'car') {
                const wheelRotation = -obstacle.speed * delta * 25; // Adjust multiplier for realistic speed
                obstacle.mesh.children.forEach(child => {
                    // Crude check if it's a wheel based on geometry type
                    if (child.geometry instanceof THREE.CylinderGeometry) {
                        child.rotation.y += wheelRotation;
                    }
                });
            }


            // Wrapping logic
            const objRightEdge = obstacle.mesh.position.x + obstacle.width / 2;
            const objLeftEdge = obstacle.mesh.position.x - obstacle.width / 2;

            if (obstacle.direction > 0 && objLeftEdge > screenBounds.right) {
                obstacle.mesh.position.x = screenBounds.left - obstacle.width / 2;
            } else if (obstacle.direction < 0 && objRightEdge < screenBounds.left) {
                obstacle.mesh.position.x = screenBounds.right + obstacle.width / 2;
            }
        });
    }
}

// --- Collision & Collection ---

// Check for coin at player's current rounded position
function checkCoinCollection() {
    if (isGameOver) return;
    const playerGridX = Math.round(chicken.position.x / TILE_SIZE) * TILE_SIZE;
    const playerGridZ = Math.round(chicken.position.z / TILE_SIZE) * TILE_SIZE;
    const key = `${playerGridX}_${playerGridZ}`;

    if (activeCoins[key]) {
        collectCoin(key);
    }
}

function collectCoin(key) {
    const coinMesh = activeCoins[key];
    // TODO: Add particle effect or sound
    scene.remove(coinMesh);
    delete activeCoins[key];
    coins++;
    coinsElement.textContent = coins;
    // Simple scale animation on score
    coinsElement.parentElement.style.transform = 'scale(1.15)';
    setTimeout(() => { coinsElement.parentElement.style.transform = 'scale(1)'; }, 150);
}


function checkLanding() {
    if (isGameOver) return;

    const landingLaneIndex = Math.round(chicken.position.z / TILE_SIZE);
    const currentLaneData = activeLanes[landingLaneIndex];

    if (!currentLaneData) {
        console.error("Landed on non-existent lane:", landingLaneIndex);
        gameOver(true); // Assume fell off world = splash
        return;
    }

    playerAttachedLog = null; // Reset attachment by default
    lastLogSpeed = 0;

    const playerBox = new THREE.Box3().setFromObject(chicken.children[0]); // Use body for collision
    playerBox.expandByScalar( - TILE_SIZE * 0.1 ); // Make hitbox slightly smaller than visual

    if (currentLaneData.type === 'road') {
        for (const obstacle of currentLaneData.obstacles) {
            const carBox = new THREE.Box3().setFromObject(obstacle.mesh.children[0]); // Collide with car body
             carBox.expandByScalar( - TILE_SIZE * 0.05 ); // Slightly smaller car hitbox
            if (playerBox.intersectsBox(carBox)) {
                console.log("Hit by car on landing!");
                gameOver();
                return;
            }
        }
        // Landed safely on road
        chicken.position.y = TILE_SIZE * 0.25; // Ensure correct height

    } else if (currentLaneData.type === 'water') {
        let onLog = false;
        for (const obstacle of currentLaneData.obstacles) {
            if (obstacle.type !== 'log') continue;

            const logBox = new THREE.Box3().setFromObject(obstacle.mesh);
             // Check if player center X is within log X bounds, and Z matches
             if (chicken.position.x >= logBox.min.x && chicken.position.x <= logBox.max.x) {
                 onLog = true;
                 playerAttachedLog = obstacle;
                 lastLogSpeed = obstacle.speed; // Store speed for next frame
                 // Snap player Y to be correctly on top of this log
                 chicken.position.y = obstacle.mesh.position.y + TILE_SIZE * 0.25 + (TILE_SIZE * 0.18 / 2);
                 console.log("Landed on log");
                 break;
             }
        }
        if (!onLog) {
            console.log("Fell in water on landing!");
            gameOver(true); // Splash death
            return;
        }
    } else if (currentLaneData.type === 'grass') {
        // Landed safely on grass
        chicken.position.y = TILE_SIZE * 0.25; // Ensure correct height
        console.log("Landed on grass");
    }
}

function checkContinuousCollision() {
    if (isGameOver || isMoving || playerAttachedLog) return; // Only check when idle on ground/road

    const currentLaneData = activeLanes[currentLaneIndex];
    if (!currentLaneData || (currentLaneData.type !== 'road' && currentLaneData.type !== 'grass')) return; // Only roads/grass matter

     const playerBox = new THREE.Box3().setFromObject(chicken.children[0]); // Body hitbox
     playerBox.expandByScalar( - TILE_SIZE * 0.1 );

    for (const obstacle of currentLaneData.obstacles) {
        if (obstacle.type !== 'car') continue;
        const carBox = new THREE.Box3().setFromObject(obstacle.mesh.children[0]); // Car body hitbox
        carBox.expandByScalar( - TILE_SIZE * 0.05 );
        if (playerBox.intersectsBox(carBox)) {
            console.log("Hit by car while idle!");
            gameOver();
            return;
        }
    }
}


// --- Game Over & Restart ---
function gameOver(splash = false) {
    if (isGameOver) return;
    isGameOver = true;
    isMoving = false;
    playerAttachedLog = null;

    // Update final scores display
    finalScoreElement.textContent = score;
    finalCoinsElement.textContent = coins;
    gameOverElement.style.display = 'flex'; // Use flex for potential centering later

    // Add a slight delay before showing the screen for impact
    setTimeout(() => {
        gameOverElement.style.opacity = '1'; // Fade in effect (needs CSS transition)
    }, 100);


    // Visual feedback
    if (splash) {
        // Splash effect - maybe tween scale and position down
        const sinkDuration = 0.5;
        let elapsed = 0;
        const startY = chicken.position.y;
        const endY = -TILE_SIZE; // Sink below ground/water level
        function sinkAnimation() {
            if (!isGameOver) return; // Stop if restarted quickly
            elapsed += clock.getDelta();
            const t = Math.min(elapsed / sinkDuration, 1.0);
            chicken.position.y = startY + (endY - startY) * t;
            chicken.rotation.z += 0.1; // Spin while sinking
            if (t < 1.0) requestAnimationFrame(sinkAnimation);
        }
        sinkAnimation();
         chickenMaterial.color.set(WATER_COLOR); // Turn blueish

    } else {
        // Hit effect - quick flatten and grey out
        chicken.scale.set(1.2, 0.1, 1.2); // Flatten
        chickenMaterial.color.set(0x777777); // Grey out
        // Maybe add a tiny shake?
        const startX = chicken.position.x;
        setTimeout(() => { chicken.position.x = startX + 0.05; }, 50);
        setTimeout(() => { chicken.position.x = startX - 0.05; }, 100);
        setTimeout(() => { chicken.position.x = startX; }, 150);
    }
}

restartButton.addEventListener('click', () => {
    // Reset State
    score = 0;
    coins = 0;
    currentLaneIndex = 0;
    playerX = 0;
    isMoving = false;
    isGameOver = false;
    playerAttachedLog = null;
    lastLogSpeed = 0;
    moveTimer = 0;

    // Clear World Objects
    for (const index in activeLanes) {
        if (activeLanes[index]) {
            activeLanes[index].obstacles.forEach(o => scene.remove(o.mesh));
            scene.remove(activeLanes[index].mesh);
        }
    }
    activeLanes = {};
    for (const key in activeCoins) {
        scene.remove(activeCoins[key]);
    }
    activeCoins = {};

    // Reset Player
    chicken.position.set(0, TILE_SIZE * 0.25, 0);
    playerTargetPos.copy(chicken.position);
    playerStartPos.copy(chicken.position);
    chickenMaterial.color.set(0xFFFFFF); // Restore original color tint if needed
    chicken.scale.set(1, 1, 1);
    chicken.rotation.set(0,0,0);


    // Reset UI
    scoreElement.textContent = score;
    coinsElement.textContent = coins;
    gameOverElement.style.display = 'none';
    gameOverElement.style.opacity = '0'; // Hide for fade-in next time

    // Regenerate World & Reset Camera
    generateInitialWorld();
    updateCameraTarget(); // Recalculate target based on reset position
    camera.position.set(playerX * TILE_SIZE + 4, 5, currentLaneIndex * TILE_SIZE + 5); // Snap camera near player start
    camera.lookAt(playerX * TILE_SIZE, 0, currentLaneIndex * TILE_SIZE);

    // Restart clock if needed
    clock.start(); // Ensure delta time is correct after potential pause
});


// --- Animation Loop ---
function animate() {
    requestAnimationFrame(animate);

    const delta = clock.getDelta();

    // Only update game logic fully if not game over
    if (!isGameOver) {
        updatePlayerPosition(delta);
        checkContinuousCollision();
    }
    // These can run even during game over for visual effects or world cleanup
  // --- continue animate function ---
    updateObstacles(delta);
    // Keep managing lanes even slightly after game over to clean up distant ones
    manageLanes();

    // Always update camera for smooth movement and potential game over views
    updateCameraPosition(delta);

    renderer.render(scene, camera);
}

// --- Handle Window Resize ---
window.addEventListener('resize', () => {
    const newAspectRatio = window.innerWidth / window.innerHeight;
    const newCameraHeight = cameraWidth / newAspectRatio;

    camera.top = newCameraHeight / 2;
    camera.bottom = newCameraHeight / -2;
    // camera.left and camera.right remain fixed by cameraWidth

    camera.updateProjectionMatrix(); // Important after changing camera properties
    renderer.setSize(window.innerWidth, window.innerHeight);
});

// --- Initial Setup Call & Start Animation ---
// Create the initial set of lanes around the player start position
generateInitialWorld();
// Start the main game loop
animate();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.