roll

<!DOCTYPE html>
<html>
<head>
    <title>Roll - Dark Descent</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <style>
        body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; touch-action: none; background-color: #fffafa; }
        #TutContainer { width: 100%; height: 100vh; }
        #scoreBoard { position: absolute; top: 20px; left: 20px; font-size: 20px; font-weight: bold; color: #333; z-index: 10; pointer-events: none; }
        #gameOverScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: none; flex-direction: column; justify-content: center; align-items: center; z-index: 100; color: white; }
        #gameOverScreen h1 { font-size: 32px; margin-bottom: 20px; text-align: center; }
        #restartBtn { padding: 15px 30px; font-size: 20px; cursor: pointer; background: #33ff33; border: none; border-radius: 5px; font-weight: bold; }
    </style>
</head>
<body>
    <div id="scoreBoard">Score: 0</div> 
    <div id="gameOverScreen">
        <h1>GAME OVER</h1>
        <button id="restartBtn">RUN AGAIN</button>
    </div>
    <div id="TutContainer"></div>  
    <script src="https://cdn.jsdelivr.net/npm/three@0.145.0/build/three.min.js"></script>   
     <script>
       let sceneWidth, sceneHeight, camera, scene, renderer, dom;
        let sun, heroRollingSpeed, clock, treesPool, treesInPath;
        let pathAngleValues;
        let globalRenderID;

        const worldRadius = 26;
        const heroRadius = 0.2;
        const heroBaseY = 2.1; 
        const gravity = 0.005;
        const particleCount = 20;

        let rollingSpeed = 0.008; 
        let treeReleaseInterval = 0.5; 
        let rightLane = 1;
        let middleLane = 0;
        let leftLane = -1;
        let lerpSpeed = 0.15; 

        let scoreText, score, hasCollided;
        let jumping = false;
        let bounceValue = 0;
        let currentLane = middleLane;
        let heroSphere;
        let rollingGroundSphere;
        let particles;
        let particleGeometry;
        let explosionPower = 1;
        let sphericalHelper;

        let touchStartX = 0, touchStartY = 0, touchEndX = 0, touchEndY = 0;
        const minSwipeDistance = 30;

        init();

        function init() {
            adjustSettings(); 
            createScene();
            update();
        }

        function adjustSettings() {
            if (window.innerWidth < 768) {
                rollingSpeed = 0.005;
                treeReleaseInterval = 1.0;
                leftLane = -0.6;
                rightLane = 0.6;
                lerpSpeed = 0.25;
            } else {
                rollingSpeed = 0.008;
                treeReleaseInterval = 0.5;
                leftLane = -1;
                rightLane = 1;
                lerpSpeed = 0.15;
            }
        }

        function createScene() {
            hasCollided = false;
            score = 0;
            treesPool = [];
            treesInPath = [];
            clock = new THREE.Clock();
            clock.start();
            
            heroRollingSpeed = (rollingSpeed * worldRadius / heroRadius) / 5;
            sphericalHelper = new THREE.Spherical();
            pathAngleValues = [1.52, 1.57, 1.62];

            sceneWidth = window.innerWidth;
            sceneHeight = window.innerHeight;

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0xfffafa);
            scene.fog = new THREE.FogExp2(0xf0fff0, 0.14);

            camera = new THREE.PerspectiveCamera(60, sceneWidth / sceneHeight, 0.1, 1000);
            renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
            renderer.setClearColor(0xfffafa, 1);
            renderer.shadowMap.enabled = true;
            renderer.shadowMap.type = THREE.PCFSoftShadowMap;
            renderer.setSize(sceneWidth, sceneHeight);

            dom = document.getElementById('TutContainer');
            dom.appendChild(renderer.domElement);
            scoreText = document.getElementById('scoreBoard');

            createTreesPool();
            addWorld();
            addLight();
            addHero();
            addExplosion();

            camera.position.z = 6.5;
            camera.position.y = 2.5;

            window.addEventListener('resize', onWindowResize, false);
            document.addEventListener('keydown', handleKeyDown);
            document.addEventListener('touchstart', handleTouchStart, {passive: false});
            document.addEventListener('touchend', handleTouchEnd, {passive: false});

            const restartBtn = document.getElementById('restartBtn');
            if (restartBtn) {
                restartBtn.addEventListener('click', () => { location.reload(); });
            }
        }

        function moveLeft() {
            if (hasCollided) return;
            if (currentLane === middleLane) currentLane = leftLane;
            else if (currentLane === rightLane) currentLane = middleLane;
            triggerJumpEffect();
        }

        function moveRight() {
            if (hasCollided) return;
            if (currentLane === middleLane) currentLane = rightLane;
            else if (currentLane === leftLane) currentLane = middleLane;
            triggerJumpEffect();
        }

        function jump() {
            if (hasCollided) return;
            if (!jumping) {
                bounceValue = 0.12;
                jumping = true;
            }
        }

        function triggerJumpEffect() {
            if (!jumping) {
                jumping = true;
                bounceValue = 0.06;
            }
        }

        function handleKeyDown(event) {
            const key = event.key.toLowerCase();
            if (key === 'a') moveLeft();
            else if (key === 'd') moveRight();
            else if (key === ' ') jump();
        }

        function handleTouchStart(e) {
            touchStartX = e.changedTouches[0].screenX;
            touchStartY = e.changedTouches[0].screenY;
        }

        function handleTouchEnd(e) {
            touchEndX = e.changedTouches[0].screenX;
            touchEndY = e.changedTouches[0].screenY;
            let dx = touchEndX - touchStartX;
            let dy = touchEndY - touchStartY;
            if (Math.abs(dx) > Math.abs(dy)) {
                if (Math.abs(dx) > minSwipeDistance) {
                    if (dx < 0) moveLeft(); else moveRight();
                }
            } else {
                if (Math.abs(dy) > minSwipeDistance) {
                    if (dy < 0) jump();
                }
            }
        }

        function update() {
            if (hasCollided) {
                doExplosionLogic();
                render();
                globalRenderID = requestAnimationFrame(update);
                return; 
            }

            // 1. Update Lighting and Atmosphere based on score
            updateAtmosphere();

            rollingGroundSphere.rotation.x += rollingSpeed;
            heroSphere.rotation.x -= heroRollingSpeed;

            if (heroSphere.position.y <= heroBaseY) {
                jumping = false;
                bounceValue = (Math.random() * 0.04) + 0.005;
            }
            heroSphere.position.y += bounceValue;
            heroSphere.position.x = THREE.MathUtils.lerp(heroSphere.position.x, currentLane, lerpSpeed); 
            bounceValue -= gravity;

            if (clock.getElapsedTime() > treeReleaseInterval) {
                clock.start();
                addPathTree();
                score += 1;
                if (scoreText) scoreText.innerHTML = "Score: " + score;
            }

            doTreeLogic();
            doExplosionLogic();
            render();
            globalRenderID = requestAnimationFrame(update);
        }

        function updateAtmosphere() {
            // Gradually get darker. Max darkness at score 200.
            let darkness = Math.min(score / 200, 1); 
            
            // Light intensity fades from 0.9 to 0.2
            sun.intensity = THREE.MathUtils.lerp(0.9, 0.2, darkness);
            
            // Background color fades from White to Deep Midnight Blue
            scene.background.setRGB(
                THREE.MathUtils.lerp(1, 0.05, darkness), 
                THREE.MathUtils.lerp(1, 0.05, darkness), 
                THREE.MathUtils.lerp(1, 0.2, darkness)
            );
            
            // Fog changes color to match
            scene.fog.color.setRGB(
                THREE.MathUtils.lerp(0.94, 0.05, darkness), 
                THREE.MathUtils.lerp(1, 0.05, darkness), 
                THREE.MathUtils.lerp(0.94, 0.2, darkness)
            );
        }

        function render() {
            renderer.render(scene, camera);
        }

        function gameOver() {
            const screen = document.getElementById('gameOverScreen');
            if (screen) screen.style.display = 'flex';
        }

        function createTreesPool() {
            var maxTreesInPool = 15;
            for (var i = 0; i < maxTreesInPool; i++) {
                treesPool.push(createTree());
            }
        }

        function addPathTree() {
            var options = [0, 1, 2];
            var lane = Math.floor(Math.random() * 3);
            addTree(true, lane);
            options.splice(lane, 1);
            if (Math.random() > 0.5) {
                lane = Math.floor(Math.random() * 2);
                addTree(true, options[lane]);
            }
        }

        function addHero() {
            var sphereGeometry = new THREE.DodecahedronGeometry(heroRadius, 1);
            var sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xe5f2f2, flatShading: true });
            heroSphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
            heroSphere.receiveShadow = true;
            heroSphere.castShadow = true;
            scene.add(heroSphere);
            heroSphere.position.set(middleLane, heroBaseY, 4.8);
            currentLane = middleLane;
        }

        function addWorld() {
            var sides = 40, tiers = 40;
            var sphereGeometry = new THREE.SphereGeometry(worldRadius, sides, tiers);
            var sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xfffafa, flatShading: true });
            
            const pos = sphereGeometry.attributes.position;
            for (let i = 0; i < pos.count; i++) {
                let x = pos.getX(i);
                let y = pos.getY(i);
                let z = pos.getZ(i);
                let vec = new THREE.Vector3(x, y, z).normalize();
                let heightValue = (Math.random() * 0.05) - 0.025;
                vec.multiplyScalar(worldRadius + heightValue);
                pos.setXYZ(i, vec.x, vec.y, vec.z);
            }
            pos.needsUpdate = true;

            rollingGroundSphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
            rollingGroundSphere.receiveShadow = true;
            rollingGroundSphere.rotation.z = -Math.PI / 2;
            scene.add(rollingGroundSphere);
            rollingGroundSphere.position.set(0, -24, 2);
            addWorldTrees();
        }

        function addLight() {
            scene.add(new THREE.HemisphereLight(0xfffafa, 0x000000, .9));
            sun = new THREE.DirectionalLight(0xcdc1c5, 0.9);
            sun.position.set(12, 6, -7);
            sun.castShadow = true;
            scene.add(sun);
        }

        function createTree() {
            var sides = 8, tiers = 6;
            var scalarMultiplier = (Math.random() * 0.15) + 0.05;
            var treeGeometry = new THREE.ConeGeometry(0.5, 1, sides, tiers);
            var treeMaterial = new THREE.MeshStandardMaterial({ color: 0x33ff33, flatShading: true });
            
            const pos = treeGeometry.attributes.position;
            blowUpTree(pos, sides, 0, scalarMultiplier);
            tightenTree(pos, sides, 1);
            blowUpTree(pos, sides, 2, scalarMultiplier * 1.1, true);
            tightenTree(pos, sides, 3);
            blowUpTree(pos, sides, 4, scalarMultiplier * 1.2);
            tightenTree(pos, sides, 5);
            pos.needsUpdate = true;

            var treeTop = new THREE.Mesh(treeGeometry, treeMaterial);
            treeTop.castShadow = true;
            treeTop.position.y = 0.9;
            treeTop.rotation.y = Math.random() * Math.PI;
            var treeTrunk = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.5), new THREE.MeshStandardMaterial({ color: 0x886633, flatShading: true }));
            treeTrunk.position.y = 0.25;
            var tree = new THREE.Object3D();
            tree.add(treeTrunk);
            tree.add(treeTop);
            return tree;
        }

        function blowUpTree(pos, sides, currentTier, scalarMultiplier, odd) {
            for (var i = 0; i < sides; i++) {
                var vertexIndex = (currentTier * sides) + 1;
                var idx = i + vertexIndex;
                let vX = pos.getX(idx);
                let vY = pos.getY(idx);
                let vZ = pos.getZ(idx);
                let vertexVector = new THREE.Vector3(vX, vY, vZ);
                let midPointVector = new THREE.Vector3(0, vY, 0);
                let offset = vertexVector.clone().sub(midPointVector);
                if (odd) {
                    if (i % 2 === 0) offset.normalize().multiplyScalar(scalarMultiplier / 6);
                    else {
                        offset.normalize().multiplyScalar(scalarMultiplier);
                        let nextY = pos.getY(idx + sides);
                        pos.setY(idx, nextY + 0.05);
                    }
                } else {
                    if (i % 2 !== 0) offset.normalize().multiplyScalar(scalarMultiplier / 6);
                    else {
                        offset.normalize().multiplyScalar(scalarMultiplier);
                        let nextY = pos.getY(idx + sides);
                        pos.setY(idx, nextY + 0.05);
                    }
                }
                let finalVec = vertexVector.add(offset);
                pos.setXYZ(idx, finalVec.x, finalVec.y, finalVec.z);
            }
        }

        function tightenTree(pos, sides, currentTier) {
            for (var i = 0; i < sides; i++) {
                var vertexIndex = (currentTier * sides) + 1;
                var idx = i + vertexIndex;
                let vX = pos.getX(idx);
                let vY = pos.getY(idx);
                let vZ = pos.getZ(idx);
                let vertexVector = new THREE.Vector3(vX, vY, vZ);
                let midPointVector = new THREE.Vector3(0, vY, 0);
                let offset = vertexVector.clone().sub(midPointVector);
                offset.normalize().multiplyScalar(0.06);
                let finalVec = vertexVector.sub(offset);
                pos.setXYZ(idx, finalVec.x, finalVec.y, finalVec.z);
            }
        }

        function addWorldTrees() {
            var numTrees = 36, gap = 6.28 / 36;
            for (var i = 0; i < numTrees; i++) {
                addTree(false, i * gap, true);
                addTree(false, i * gap, false);
            }
        }

        function addTree(inPath, row, isLeft) {
            var newTree;
            if (inPath) {
                if (treesPool.length == 0) return;
                newTree = treesPool.pop();
                newTree.visible = true;
                
                // Reset rotations in case it was a fallen tree
                newTree.rotation.set(0,0,0);

                // Randomly decide if this tree falls (30% chance)
                newTree.userData.isFalling = Math.random() < 0.3;
                newTree.userData.fallRotation = 0;

                treesInPath.push(newTree);
                sphericalHelper.set(worldRadius - 0.3, pathAngleValues[row], -rollingGroundSphere.rotation.x + 4);
            } else {
                newTree = createTree();
                var forestAreaAngle = isLeft ? 1.68 + Math.random() * 0.1 : 1.46 - Math.random() * 0.1;
                sphericalHelper.set(worldRadius - 0.3, forestAreaAngle, row);
            }
            newTree.position.setFromSpherical(sphericalHelper);
            var rollingGroundVector = rollingGroundSphere.position.clone().normalize();
            var treeVector = newTree.position.clone().normalize();
            newTree.quaternion.setFromUnitVectors(treeVector, rollingGroundVector);
            newTree.rotation.x += (Math.random() * (2 * Math.PI / 10)) + -Math.PI / 10;
            rollingGroundSphere.add(newTree);
        }

        function addExplosion() {
            particleGeometry = new THREE.BufferGeometry();
            const positions = new Float32Array(particleCount * 3);
            particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
            var pMaterial = new THREE.PointsMaterial({ color: 0xfffafa, size: 0.2 });
            particles = new THREE.Points(particleGeometry, pMaterial);
            scene.add(particles);
            particles.visible = false;
        }

        function explode() {
            particles.position.set(heroSphere.position.x, 2, 4.8);
            const pos = particleGeometry.attributes.position;
            for (var i = 0; i < particleCount; i++) {
                pos.setXYZ(i, -0.2 + Math.random() * 0.4, -0.2 + Math.random() * 0.4, -0.2 + Math.random() * 0.4);
            }
            pos.needsUpdate = true;
            explosionPower = 1.07;
            particles.visible = true;
        }

        function doTreeLogic() {
            var treePos = new THREE.Vector3(), treesToRemove = [];
            treesInPath.forEach(function (oneTree) {
                treePos.setFromMatrixPosition(oneTree.matrixWorld);
                
                // Animation: If it's a falling tree, tip it over as it gets closer
                if (oneTree.userData.isFalling) {
                    if (oneTree.userData.fallRotation < Math.PI / 2) {
                        oneTree.userData.fallRotation += 0.05;
                        oneTree.rotation.z = oneTree.userData.fallRotation;
                    }
                }

                if (treePos.z > 6 && oneTree.visible) {
                    treesToRemove.push(oneTree);
                } else if (treePos.distanceTo(heroSphere.position) <= 0.6) {
                    
                    // SPECIAL COLLISION: If the tree is fallen, you can jump over it!
                    if (oneTree.userData.isFalling && heroSphere.position.y > heroBaseY + 0.2) {
                        // Safe! User jumped over the fallen tree
                    } else {
                        hasCollided = true;
                        explode();
                        gameOver();
                    }
                }
            });
            treesToRemove.forEach(function (element) {
                var fromWhere = treesInPath.indexOf(element);
                treesInPath.splice(fromWhere, 1);
                treesPool.push(element);
                element.visible = false;
            });
        }

        function doExplosionLogic() {
            if (!particles.visible) return;
            const pos = particleGeometry.attributes.position;
            for (var i = 0; i < particleCount; i++) {
                let x = pos.getX(i) * explosionPower;
                let y = pos.getY(i) * explosionPower;
                let z = pos.getZ(i) * explosionPower;
                pos.setXYZ(i, x, y, z);
            }
            pos.needsUpdate = true;
            if (explosionPower > 1.005) explosionPower -= 0.001;
            else particles.visible = false;
        }

        function onWindowResize() {
            sceneHeight = window.innerHeight;
            sceneWidth = window.innerWidth;
            renderer.setSize(sceneWidth, sceneHeight);
            camera.aspect = sceneWidth / sceneHeight;
            camera.updateProjectionMatrix();
        } 
    </script>
</body>
</html>