๐ŸŽฎArcadeLab

๐Ÿ Realistic 3D Cricket Game

by SuperWolf11
1819 lines66.2 KB๐Ÿ› ๏ธ Three.js (3D graphics)
โ–ถ Play
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>๐Ÿ Realistic 3D Cricket Game</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none;
        }
        body {
            overflow: hidden;
            background: #0a0a1a;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        canvas {
            display: block;
        }

        /* HUD overlay */
        #hud {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 10;
        }

        /* Scoreboard */
        #scoreboard {
            position: absolute;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.85);
            border: 2px solid rgba(255, 215, 0, 0.4);
            border-radius: 16px;
            padding: 14px 28px;
            display: flex;
            align-items: center;
            gap: 30px;
            backdrop-filter: blur(12px);
            box-shadow: 0 8px 40px rgba(0, 0, 0, 0.7);
            pointer-events: auto;
            color: #fff;
            font-size: 15px;
            min-width: 480px;
            justify-content: center;
            flex-wrap: wrap;
        }
        #scoreboard .team {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        #scoreboard .team .name {
            font-weight: 700;
            font-size: 18px;
            letter-spacing: 0.5px;
            color: #f0f0f0;
        }
        #scoreboard .team .score {
            font-weight: 800;
            font-size: 28px;
            color: #ffd700;
            min-width: 60px;
            text-align: center;
        }
        #scoreboard .team .wickets {
            font-size: 18px;
            color: #ff6b6b;
        }
        #scoreboard .team .overs {
            font-size: 14px;
            color: #aaa;
            margin-left: 4px;
        }
        #scoreboard .vs {
            color: #666;
            font-weight: 300;
            font-size: 16px;
        }
        #scoreboard .status {
            color: #7bed9f;
            font-size: 13px;
            font-weight: 600;
            background: rgba(123, 237, 159, 0.15);
            padding: 4px 16px;
            border-radius: 20px;
            border: 1px solid rgba(123, 237, 159, 0.3);
        }
        #scoreboard .batting-info {
            color: #bbb;
            font-size: 13px;
            display: flex;
            gap: 16px;
            align-items: center;
        }
        #scoreboard .batting-info .batsman {
            color: #7bed9f;
            font-weight: 600;
        }
        #scoreboard .batting-info .bowler {
            color: #ffd93d;
            font-weight: 600;
        }

        /* Control buttons */
        #controls {
            position: absolute;
            bottom: 30px;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            gap: 18px;
            pointer-events: auto;
            flex-wrap: wrap;
            justify-content: center;
            background: rgba(0, 0, 0, 0.7);
            padding: 14px 24px;
            border-radius: 50px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.08);
        }
        #controls button {
            padding: 10px 22px;
            border: none;
            border-radius: 30px;
            font-size: 14px;
            font-weight: 700;
            cursor: pointer;
            transition: all 0.2s ease;
            pointer-events: auto;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            background: rgba(255, 255, 255, 0.08);
            color: #ccc;
            border: 1px solid rgba(255, 255, 255, 0.06);
        }
        #controls button:hover {
            transform: scale(1.05);
            background: rgba(255, 255, 255, 0.18);
            color: #fff;
        }
        #controls button.primary {
            background: #ffd700;
            color: #1a1a2e;
            border-color: #ffd700;
        }
        #controls button.primary:hover {
            background: #ffe44d;
            box-shadow: 0 0 30px rgba(255, 215, 0, 0.3);
        }
        #controls button.danger {
            background: #ff6b6b;
            color: #1a1a2e;
            border-color: #ff6b6b;
        }
        #controls button.danger:hover {
            background: #ff8787;
            box-shadow: 0 0 30px rgba(255, 107, 107, 0.3);
        }
        #controls button.green {
            background: #2ecc71;
            color: #1a1a2e;
            border-color: #2ecc71;
        }
        #controls button.green:hover {
            background: #55e68a;
            box-shadow: 0 0 30px rgba(46, 204, 113, 0.3);
        }
        #controls button:disabled {
            opacity: 0.3;
            cursor: not-allowed;
            transform: none !important;
        }
        #controls .key-hint {
            color: #666;
            font-size: 10px;
            font-weight: 400;
            display: block;
            text-align: center;
            margin-top: 2px;
        }

        /* Power meter */
        #power-meter {
            position: absolute;
            right: 30px;
            top: 50%;
            transform: translateY(-50%);
            width: 24px;
            height: 240px;
            background: rgba(0, 0, 0, 0.7);
            border-radius: 20px;
            border: 2px solid rgba(255, 215, 0, 0.3);
            overflow: hidden;
            pointer-events: none;
            backdrop-filter: blur(4px);
            opacity: 0;
            transition: opacity 0.3s ease;
        }
        #power-meter.active {
            opacity: 1;
        }
        #power-meter .fill {
            position: absolute;
            bottom: 0;
            width: 100%;
            background: linear-gradient(to top, #2ecc71, #ffd700, #ff6b6b);
            height: 0%;
            transition: height 0.05s linear;
            border-radius: 20px;
        }
        #power-meter .label {
            position: absolute;
            top: -30px;
            left: 50%;
            transform: translateX(-50%);
            color: #fff;
            font-size: 11px;
            font-weight: 600;
            text-shadow: 0 2px 10px rgba(0, 0, 0, 0.8);
            white-space: nowrap;
        }

        /* Ball indicator */
        #ball-indicator {
            position: absolute;
            bottom: 120px;
            left: 50%;
            transform: translateX(-50%);
            color: #fff;
            font-size: 14px;
            background: rgba(0, 0, 0, 0.6);
            padding: 6px 20px;
            border-radius: 20px;
            backdrop-filter: blur(4px);
            border: 1px solid rgba(255, 255, 255, 0.06);
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.3s ease;
            text-align: center;
            font-weight: 500;
        }
        #ball-indicator.active {
            opacity: 1;
        }

        /* Camera selector */
        #camera-selector {
            position: absolute;
            top: 100px;
            right: 20px;
            display: flex;
            flex-direction: column;
            gap: 8px;
            pointer-events: auto;
        }
        #camera-selector button {
            padding: 6px 12px;
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 8px;
            background: rgba(0, 0, 0, 0.6);
            color: #aaa;
            font-size: 11px;
            cursor: pointer;
            transition: all 0.2s ease;
            backdrop-filter: blur(4px);
            font-weight: 600;
            letter-spacing: 0.3px;
        }
        #camera-selector button:hover {
            background: rgba(255, 215, 0, 0.2);
            color: #fff;
            border-color: rgba(255, 215, 0, 0.3);
        }
        #camera-selector button.active {
            background: rgba(255, 215, 0, 0.25);
            color: #ffd700;
            border-color: #ffd700;
        }

        /* Match info */
        #match-info {
            position: absolute;
            top: 95px;
            left: 20px;
            color: #aaa;
            font-size: 12px;
            background: rgba(0, 0, 0, 0.6);
            padding: 10px 16px;
            border-radius: 12px;
            backdrop-filter: blur(4px);
            border: 1px solid rgba(255, 255, 255, 0.04);
            pointer-events: none;
            line-height: 1.6;
        }
        #match-info .highlight {
            color: #ffd700;
            font-weight: 600;
        }

        /* Message popup */
        #message {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #fff;
            font-size: 48px;
            font-weight: 800;
            text-shadow: 0 4px 40px rgba(0, 0, 0, 0.9);
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.5s ease;
            text-align: center;
            background: rgba(0, 0, 0, 0.5);
            padding: 30px 50px;
            border-radius: 24px;
            backdrop-filter: blur(8px);
            border: 2px solid rgba(255, 215, 0, 0.1);
        }
        #message.active {
            opacity: 1;
        }
        #message .sub {
            font-size: 20px;
            font-weight: 400;
            color: #ccc;
            margin-top: 8px;
        }

        @media (max-width: 800px) {
            #scoreboard {
                min-width: auto;
                width: 92%;
                padding: 10px 16px;
                gap: 12px;
                font-size: 12px;
                top: 10px;
            }
            #scoreboard .team .score {
                font-size: 22px;
                min-width: 40px;
            }
            #scoreboard .team .name {
                font-size: 14px;
            }
            #controls {
                bottom: 16px;
                padding: 10px 14px;
                gap: 10px;
                border-radius: 30px;
                width: 95%;
                flex-wrap: wrap;
            }
            #controls button {
                padding: 6px 14px;
                font-size: 11px;
            }
            #power-meter {
                right: 12px;
                height: 160px;
                width: 18px;
            }
            #camera-selector {
                top: 80px;
                right: 10px;
            }
            #camera-selector button {
                padding: 4px 8px;
                font-size: 9px;
            }
            #match-info {
                display: none;
            }
            #message {
                font-size: 28px;
                padding: 20px 30px;
            }
            #message .sub {
                font-size: 16px;
            }
            #ball-indicator {
                bottom: 80px;
                font-size: 12px;
                padding: 4px 14px;
            }
        }
    </style>
</head>
<body>

    <!-- HUD -->
    <div id="hud">
        <!-- Scoreboard -->
        <div id="scoreboard">
            <div class="team">
                <span class="name" id="team1-name">INDIA</span>
                <span class="score" id="team1-score">0</span>
                <span class="wickets" id="team1-wickets">/0</span>
                <span class="overs" id="team1-overs">(0.0)</span>
            </div>
            <span class="vs">vs</span>
            <div class="team">
                <span class="name" id="team2-name">AUS</span>
                <span class="score" id="team2-score">0</span>
                <span class="wickets" id="team2-wickets">/0</span>
                <span class="overs" id="team2-overs">(0.0)</span>
            </div>
            <span class="status" id="match-status">โ— Live</span>
            <div class="batting-info">
                <span><span class="batsman" id="current-batsman">Kohli</span> โœฆ</span>
                <span><span class="bowler" id="current-bowler">Starc</span> โšก</span>
            </div>
        </div>

        <!-- Match info -->
        <div id="match-info">
            <div>Match: <span class="highlight" id="match-type">T20</span></div>
            <div>Overs: <span class="highlight" id="match-overs">0.0</span></div>
            <div>Runs: <span class="highlight" id="match-runs">0</span></div>
            <div>Wickets: <span class="highlight" id="match-wickets">0</span></div>
            <div>Balls: <span class="highlight" id="match-balls">0</span></div>
        </div>

        <!-- Power meter -->
        <div id="power-meter">
            <div class="label">POWER</div>
            <div class="fill" id="power-fill" style="height:0%"></div>
        </div>

        <!-- Ball indicator -->
        <div id="ball-indicator">๐Ÿ Ready to bowl</div>

        <!-- Message -->
        <div id="message">
            <span id="msg-text">๐ŸŽ‰ SIX!</span>
            <div class="sub" id="msg-sub">What a shot!</div>
        </div>

        <!-- Camera selector -->
        <div id="camera-selector">
            <button data-cam="broadcast" class="active">๐Ÿ“บ Broadcast</button>
            <button data-cam="bowler">๐ŸŽฏ Bowler</button>
            <button data-cam="batsman">๐Ÿ Batsman</button>
            <button data-cam="behind">๐Ÿ‘€ Behind Stumps</button>
            <button data-cam="field">๐ŸŒฟ Field</button>
            <button data-cam="ball">โšช Ball Follow</button>
        </div>

        <!-- Controls -->
        <div id="controls">
            <button class="primary" id="btn-bowl">โšก Bowl</button>
            <button class="green" id="btn-shot">๐Ÿ Shot</button>
            <button id="btn-defend">๐Ÿ›ก๏ธ Defend</button>
            <button id="btn-leave">๐Ÿšถ Leave</button>
            <button class="danger" id="btn-reset">๐Ÿ”„ Reset</button>
            <button id="btn-toss">๐Ÿช™ Toss</button>
        </div>
    </div>

    <!-- Three.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js">
    </script>
    <script>
        // ============================================================
        //  CRICKET GAME โ€” Full 3D Simulation
        // ============================================================

        // -------- GLOBALS --------
        const TEAMS = {
            INDIA: { name: 'INDIA', color: 0x1a5c9e, players: ['Kohli', 'Sharma', 'Rahul', 'Pandya', 'Jadeja', 'Bumrah'] },
            AUS: { name: 'AUS', color: 0xf7c93e, players: ['Smith', 'Warner', 'Labuschagne', 'Maxwell', 'Starc', 'Cummins'] },
            ENG: { name: 'ENG', color: 0x1a3d7c, players: ['Root', 'Buttler', 'Stokes', 'Bairstow', 'Rashid', 'Archer'] },
            PAK: { name: 'PAK', color: 0x1a6b1a, players: ['Babar', 'Rizwan', 'Shaheen', 'Hasan', 'Imad', 'Shadab'] },
            NZ: { name: 'NZ', color: 0x1a1a1a, players: ['Kane', 'Conway', 'Boult', 'Southee', 'Santner', 'Latham'] },
            SA: { name: 'SA', color: 0x1a7a3a, players: ['De Kock', 'Bavuma', 'Rabada', 'Nortje', 'Maharaj', 'Miller'] },
        };
        const TEAM_NAMES = Object.keys(TEAMS);

        // Game state
        const state = {
            team1: 'INDIA',
            team2: 'AUS',
            batting: 'INDIA',
            bowling: 'AUS',
            runs: 0,
            wickets: 0,
            overs: 0,
            balls: 0,
            totalRuns: 0,
            totalWickets: 0,
            totalOvers: 0,
            innings: 1,
            maxOvers: 20,
            matchType: 'T20',
            isBowling: false,
            isBatting: false,
            isRunning: false,
            ballInPlay: false,
            deliveryType: 'fast',
            shotType: 'drive',
            power: 0.5,
            isCharging: false,
            chargeStart: 0,
            targetRuns: 0,
            matchOver: false,
            inningsBreak: false,
            batsman: 0,
            bowler: 0,
            striker: 'Kohli',
            nonStriker: 'Sharma',
            currentBowler: 'Starc',
            fielders: [],
            deliveries: 0,
            extras: { wide: 0, noBall: 0, bye: 0, legBye: 0 },
            boundary: 0,
            sixes: 0,
            dots: 0,
        };

        // -------- THREE.JS SETUP --------
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x1a2a4a);
        scene.fog = new THREE.Fog(0x1a2a4a, 120, 250);

        const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 500);
        camera.position.set(40, 30, 60);
        camera.lookAt(0, 0, 0);

        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 1.2;
        document.body.prepend(renderer.domElement);

        // -------- LIGHTS --------
        const ambient = new THREE.AmbientLight(0x334466, 0.4);
        scene.add(ambient);

        const sun = new THREE.DirectionalLight(0xffeedd, 1.8);
        sun.position.set(40, 60, 30);
        sun.castShadow = true;
        sun.shadow.mapSize.width = 2048;
        sun.shadow.mapSize.height = 2048;
        sun.shadow.camera.near = 0.5;
        sun.shadow.camera.far = 150;
        sun.shadow.camera.left = -80;
        sun.shadow.camera.right = 80;
        sun.shadow.camera.top = 80;
        sun.shadow.camera.bottom = -80;
        scene.add(sun);

        const fill = new THREE.DirectionalLight(0x8888ff, 0.3);
        fill.position.set(-30, 20, -40);
        scene.add(fill);

        const rim = new THREE.DirectionalLight(0xffaa44, 0.2);
        rim.position.set(-20, 10, 50);
        scene.add(rim);

        // Stadium lights
        for (let i = 0; i < 8; i++) {
            const angle = (i / 8) * Math.PI * 2;
            const x = Math.cos(angle) * 75;
            const z = Math.sin(angle) * 75;
            const light = new THREE.PointLight(0xffeedd, 0.15, 100);
            light.position.set(x, 45, z);
            scene.add(light);
        }

        // -------- GROUND / STADIUM --------
        function createStadium() {
            const group = new THREE.Group();

            // Ground plane
            const groundGeo = new THREE.PlaneGeometry(180, 180);
            const groundMat = new THREE.MeshStandardMaterial({
                color: 0x2d5a1e,
                roughness: 0.9,
                metalness: 0.0
            });
            const ground = new THREE.Mesh(groundGeo, groundMat);
            ground.rotation.x = -Math.PI / 2;
            ground.position.y = -0.05;
            ground.receiveShadow = true;
            group.add(ground);

            // Pitch (strip)
            const pitchMat = new THREE.MeshStandardMaterial({ color: 0x8a7a5a, roughness: 0.8 });
            const pitch = new THREE.Mesh(new THREE.PlaneGeometry(4, 22), pitchMat);
            pitch.rotation.x = -Math.PI / 2;
            pitch.position.set(0, 0.01, 0);
            pitch.receiveShadow = true;
            group.add(pitch);

            // Pitch markings
            const markMat = new THREE.MeshStandardMaterial({ color: 0xffffff, transparent: true, opacity: 0.15 });
            for (let i = -5; i <= 5; i++) {
                const mark = new THREE.Mesh(new THREE.PlaneGeometry(0.02, 0.5), markMat);
                mark.rotation.x = -Math.PI / 2;
                mark.position.set(0, 0.02, i * 2);
                group.add(mark);
            }

            // Boundary rope
            const ropePoints = [];
            const segments = 64;
            for (let i = 0; i <= segments; i++) {
                const theta = (i / segments) * Math.PI * 2;
                const r = 28;
                ropePoints.push(new THREE.Vector3(Math.cos(theta) * r, 0.08, Math.sin(theta) * r));
            }
            const ropeGeo = new THREE.BufferGeometry().setFromPoints(ropePoints);
            const ropeMat = new THREE.LineBasicMaterial({ color: 0xff4444, linewidth: 2 });
            const rope = new THREE.Line(ropeGeo, ropeMat);
            group.add(rope);

            // Boundary flags
            for (let i = 0; i < 12; i++) {
                const theta = (i / 12) * Math.PI * 2;
                const r = 28;
                const flag = new THREE.Mesh(
                    new THREE.CylinderGeometry(0.06, 0.06, 0.8, 4),
                    new THREE.MeshStandardMaterial({ color: 0xff4444 })
                );
                flag.position.set(Math.cos(theta) * r, 0.4, Math.sin(theta) * r);
                flag.castShadow = true;
                group.add(flag);
                const top = new THREE.Mesh(
                    new THREE.SphereGeometry(0.08, 6, 6),
                    new THREE.MeshStandardMaterial({ color: 0xffdd44 })
                );
                top.position.set(Math.cos(theta) * r, 0.85, Math.sin(theta) * r);
                group.add(top);
            }

            // Stumps (two ends)
            const stumpMat = new THREE.MeshStandardMaterial({ color: 0xd4a574, roughness: 0.6 });
            const stumpPositions = [
                [0, 0, -11],
                [0, 0, 11]
            ];
            stumpPositions.forEach((pos) => {
                for (let i = -1; i <= 1; i++) {
                    const stump = new THREE.Mesh(
                        new THREE.CylinderGeometry(0.04, 0.06, 0.8, 8),
                        stumpMat
                    );
                    stump.position.set(pos[0] + i * 0.08, 0.4, pos[2]);
                    stump.castShadow = true;
                    group.add(stump);
                    // Bail
                    if (i < 1) {
                        const bail = new THREE.Mesh(
                            new THREE.CylinderGeometry(0.015, 0.015, 0.2, 4),
                            new THREE.MeshStandardMaterial({ color: 0xffaa44 })
                        );
                        bail.rotation.z = Math.PI / 2;
                        bail.position.set(pos[0] + i * 0.08 + 0.1, 0.82, pos[2]);
                        group.add(bail);
                    }
                }
            });

            // Crease lines
            const creaseMat = new THREE.MeshStandardMaterial({ color: 0xffffff, transparent: true, opacity: 0.3 });
            for (let z of [-11, 11]) {
                const crease = new THREE.Mesh(new THREE.PlaneGeometry(0.8, 0.02), creaseMat);
                crease.rotation.x = -Math.PI / 2;
                crease.position.set(0, 0.02, z);
                group.add(crease);
            }

            // Stadium stands (simplified)
            const standMat = new THREE.MeshStandardMaterial({ color: 0x2a2a3a, roughness: 0.9 });
            const roofMat = new THREE.MeshStandardMaterial({ color: 0x3a3a5a, roughness: 0.7, metalness: 0.1 });
            for (let i = 0; i < 16; i++) {
                const theta = (i / 16) * Math.PI * 2;
                const r1 = 32 + Math.sin(i * 1.7) * 3;
                const r2 = 45 + Math.sin(i * 1.7 + 0.5) * 4;
                const r3 = 52 + Math.sin(i * 1.7 + 1) * 3;
                // Stand section
                const shape = new THREE.Shape();
                shape.moveTo(Math.cos(theta) * r1, Math.sin(theta) * r1);
                shape.lineTo(Math.cos(theta + 0.12) * r1, Math.sin(theta + 0.12) * r1);
                shape.lineTo(Math.cos(theta + 0.14) * r2, Math.sin(theta + 0.14) * r2);
                shape.lineTo(Math.cos(theta + 0.02) * r2, Math.sin(theta + 0.02) * r2);
                shape.lineTo(Math.cos(theta) * r1, Math.sin(theta) * r1);

                const extSettings = { depth: 3 + Math.sin(i * 2) * 1.5, bevelEnabled: false };
                const standGeo = new THREE.ExtrudeGeometry(shape, extSettings);
                const standMesh = new THREE.Mesh(standGeo, standMat);
                standMesh.position.y = 0;
                standMesh.rotation.y = 0;
                standMesh.castShadow = true;
                standMesh.receiveShadow = true;
                group.add(standMesh);

                // Roof
                const roofShape = new THREE.Shape();
                const rr1 = r2 + 1;
                const rr2 = r3 + 2;
                roofShape.moveTo(Math.cos(theta) * rr1, Math.sin(theta) * rr1);
                roofShape.lineTo(Math.cos(theta + 0.1) * rr1, Math.sin(theta + 0.1) * rr1);
                roofShape.lineTo(Math.cos(theta + 0.12) * rr2, Math.sin(theta + 0.12) * rr2);
                roofShape.lineTo(Math.cos(theta + 0.02) * rr2, Math.sin(theta + 0.02) * rr2);
                roofShape.lineTo(Math.cos(theta) * rr1, Math.sin(theta) * rr1);
                const roofGeo2 = new THREE.ExtrudeGeometry(roofShape, { depth: 0.5, bevelEnabled: false });
                const roofMesh = new THREE.Mesh(roofGeo2, roofMat);
                roofMesh.position.y = 3.5 + Math.sin(i * 1.3) * 0.5;
                roofMesh.castShadow = true;
                roofMesh.receiveShadow = true;
                group.add(roofMesh);
            }

            // Scoreboard (digital display)
            const boardGroup = new THREE.Group();
            const boardMat = new THREE.MeshStandardMaterial({ color: 0x111122, roughness: 0.3, metalness: 0.6 });
            const board = new THREE.Mesh(new THREE.BoxGeometry(3, 1.8, 0.3), boardMat);
            board.position.set(-20, 6, 25);
            board.castShadow = true;
            group.add(board);

            // Screen glow
            const screenMat = new THREE.MeshStandardMaterial({
                color: 0x00ff88,
                emissive: 0x00ff88,
                emissiveIntensity: 0.15,
                transparent: true,
                opacity: 0.6
            });
            const screen = new THREE.Mesh(new THREE.PlaneGeometry(2.6, 1.4), screenMat);
            screen.position.set(-20, 6, 25.16);
            group.add(screen);

            return group;
        }

        const stadium = createStadium();
        scene.add(stadium);

        // -------- PLAYER CREATION --------
        function createPlayer(teamColor, isBatsman = false, isBowler = false) {
            const group = new THREE.Group();

            // Body (torso)
            const bodyMat = new THREE.MeshStandardMaterial({
                color: teamColor,
                roughness: 0.5,
                metalness: 0.05
            });
            const body = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.35, 0.9, 8), bodyMat);
            body.position.y = 0.95;
            body.castShadow = true;
            group.add(body);

            // Head
            const headMat = new THREE.MeshStandardMaterial({
                color: 0xf5d0b8,
                roughness: 0.6
            });
            const head = new THREE.Mesh(new THREE.SphereGeometry(0.22, 8, 8), headMat);
            head.position.y = 1.55;
            head.castShadow = true;
            group.add(head);

            // Hair (simplified)
            const hairMat = new THREE.MeshStandardMaterial({ color: 0x2a1a0a, roughness: 0.8 });
            const hair = new THREE.Mesh(new THREE.SphereGeometry(0.23, 8, 8, 0, Math.PI * 2, 0, Math.PI / 2), hairMat);
            hair.position.y = 1.63;
            hair.scale.set(1, 0.6, 1);
            group.add(hair);

            // Left arm
            const armMat = new THREE.MeshStandardMaterial({ color: 0xf5d0b8, roughness: 0.5 });
            const lArm = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.07, 0.5, 6), armMat);
            lArm.position.set(-0.45, 1.2, 0);
            lArm.rotation.z = 0.3;
            lArm.castShadow = true;
            group.add(lArm);

            // Right arm
            const rArm = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.07, 0.5, 6), armMat);
            rArm.position.set(0.45, 1.2, 0);
            rArm.rotation.z = -0.3;
            rArm.castShadow = true;
            group.add(rArm);

            // Legs
            const legMat = new THREE.MeshStandardMaterial({
                color: 0x2a2a3a,
                roughness: 0.7
            });
            const lLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.1, 0.5, 6), legMat);
            lLeg.position.set(-0.15, 0.25, 0);
            lLeg.castShadow = true;
            group.add(lLeg);

            const rLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.1, 0.5, 6), legMat);
            rLeg.position.set(0.15, 0.25, 0);
            rLeg.castShadow = true;
            group.add(rLeg);

            // Shoes
            const shoeMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9 });
            const lShoe = new THREE.Mesh(new THREE.SphereGeometry(0.08, 6, 6), shoeMat);
            lShoe.position.set(-0.15, 0.02, 0.08);
            group.add(lShoe);
            const rShoe = new THREE.Mesh(new THREE.SphereGeometry(0.08, 6, 6), shoeMat);
            rShoe.position.set(0.15, 0.02, 0.08);
            group.add(rShoe);

            // Bat (for batsman)
            if (isBatsman) {
                const batMat = new THREE.MeshStandardMaterial({ color: 0xd4a574, roughness: 0.5 });
                const handleMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2a, roughness: 0.7 });
                const blade = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.35, 0.015), batMat);
                blade.position.set(0.55, 1.0, 0.2);
                blade.rotation.z = -0.2;
                blade.rotation.x = -0.3;
                group.add(blade);
                const handle = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.2, 6), handleMat);
                handle.position.set(0.55, 1.22, 0.2);
                handle.rotation.z = -0.2;
                handle.rotation.x = -0.3;
                group.add(handle);
                // Helmet
                const helmMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.3, roughness: 0.3 });
                const helm = new THREE.Mesh(new THREE.SphereGeometry(0.24, 8, 8, 0, Math.PI * 2, 0, Math.PI / 2.5), helmMat);
                helm.position.y = 1.6;
                helm.scale.set(1, 0.5, 1);
                group.add(helm);
                // Helmet grill
                const grillMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.5 });
                const grill = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.06, 0.02), grillMat);
                grill.position.set(0, 1.48, 0.2);
                group.add(grill);
            }

            // Gloves (for wicketkeeper/batsman)
            if (isBatsman) {
                const gloveMat = new THREE.MeshStandardMaterial({ color: 0xcc3333, roughness: 0.6 });
                const g1 = new THREE.Mesh(new THREE.SphereGeometry(0.065, 6, 6), gloveMat);
                g1.position.set(0.5, 1.35, 0.15);
                group.add(g1);
                const g2 = new THREE.Mesh(new THREE.SphereGeometry(0.065, 6, 6), gloveMat);
                g2.position.set(-0.5, 1.35, 0.15);
                group.add(g2);
            }

            // Pads (for batsman/wk)
            if (isBatsman) {
                const padMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.7 });
                const p1 = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.3, 0.04), padMat);
                p1.position.set(-0.15, 0.5, 0.12);
                group.add(p1);
                const p2 = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.3, 0.04), padMat);
                p2.position.set(0.15, 0.5, 0.12);
                group.add(p2);
            }

            group.userData = { body, head, lArm, rArm, lLeg, rLeg };
            return group;
        }

        // -------- CREATE TEAMS --------
        const teamPlayers = {};
        const playerPositions = [];

        function setupTeams() {
            const team1Color = TEAMS[state.team1].color;
            const team2Color = TEAMS[state.team2].color;

            // Fielding team (team2) - place around the field
            const fieldPositions = [
                [-12, 0, -8],
                [-10, 0, -5],
                [-8, 0, -2],
                [8, 0, -2],
                [10, 0, -5],
                [12, 0, -8],
                [-6, 0, 8],
                [6, 0, 8],
                [-14, 0, 2],
                [14, 0, 2],
                [0, 0, 14]
            ];

            // Clear old players
            const toRemove = [];
            scene.children.forEach(child => {
                if (child.userData && child.userData.isPlayer) toRemove.push(child);
            });
            toRemove.forEach(child => scene.remove(child));

            // Create batsman (team1)
            const batsman = createPlayer(team1Color, true, false);
            batsman.position.set(0.3, 0, -10.5);
            batsman.rotation.y = 0;
            batsman.userData.isPlayer = true;
            batsman.userData.role = 'batsman';
            batsman.userData.team = state.team1;
            scene.add(batsman);
            state.batsmanObj = batsman;

            // Non-striker
            const nonStriker = createPlayer(team1Color, true, false);
            nonStriker.position.set(-1.2, 0, 9.5);
            nonStriker.rotation.y = Math.PI;
            nonStriker.userData.isPlayer = true;
            nonStriker.userData.role = 'nonstriker';
            nonStriker.userData.team = state.team1;
            scene.add(nonStriker);
            state.nonStrikerObj = nonStriker;

            // Bowler (team2)
            const bowler = createPlayer(team2Color, false, true);
            bowler.position.set(0, 0, 12.5);
            bowler.rotation.y = Math.PI;
            bowler.userData.isPlayer = true;
            bowler.userData.role = 'bowler';
            bowler.userData.team = state.team2;
            scene.add(bowler);
            state.bowlerObj = bowler;

            // Wicketkeeper (team2)
            const wk = createPlayer(team2Color, true, false);
            wk.position.set(0.3, 0, -10.8);
            wk.rotation.y = 0;
            wk.userData.isPlayer = true;
            wk.userData.role = 'wicketkeeper';
            wk.userData.team = state.team2;
            scene.add(wk);
            state.wkObj = wk;

            // Fielders (team2)
            state.fielderObjs = [];
            fieldPositions.forEach((pos, i) => {
                const f = createPlayer(team2Color, false, false);
                f.position.set(pos[0], 0, pos[1]);
                f.rotation.y = Math.atan2(-pos[0], -pos[1]);
                f.userData.isPlayer = true;
                f.userData.role = 'fielder';
                f.userData.team = state.team2;
                f.userData.homePos = new THREE.Vector3(pos[0], 0, pos[1]);
                f.userData.speed = 0.5 + Math.random() * 0.3;
                scene.add(f);
                state.fielderObjs.push(f);
            });

            // Update UI
            document.getElementById('team1-name').textContent = state.team1;
            document.getElementById('team2-name').textContent = state.team2;
        }

        // -------- BALL --------
        const ballMat = new THREE.MeshStandardMaterial({
            color: 0xcc3333,
            roughness: 0.5,
            metalness: 0.1
        });
        const ball = new THREE.Mesh(new THREE.SphereGeometry(0.055, 16, 16), ballMat);
        ball.position.set(0, 0.5, 13);
        ball.castShadow = true;
        ball.userData.velocity = new THREE.Vector3(0, 0, 0);
        ball.userData.trail = [];
        scene.add(ball);

        // Ball seam
        const seamMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.3 });
        const seam = new THREE.Mesh(new THREE.TorusGeometry(0.055, 0.008, 8, 16), seamMat);
        seam.rotation.x = Math.PI / 2;
        seam.position.y = 0;
        ball.add(seam);

        // -------- PHYSICS --------
        const GRAVITY = -9.8;
        const BOUNCE_COEFF = 0.55;
        const FRICTION = 0.97;
        const AIR_RESISTANCE = 0.999;

        let ballInPlay = false;
        let ballTrajectory = [];
        let deliveryData = {
            speed: 0,
            swing: 0,
            seam: 0,
            spin: 0,
            type: 'fast',
            targetX: 0,
            targetZ: -11,
            releasePoint: new THREE.Vector3(0, 2.2, 12.5)
        };

        function resetBall() {
            ball.position.set(0, 0.5, 13);
            ball.userData.velocity.set(0, 0, 0);
            ballInPlay = false;
            ball.userData.trail = [];
            document.getElementById('ball-indicator').classList.remove('active');
            document.getElementById('ball-indicator').textContent = '๐Ÿ Ready to bowl';
        }

        function bowlBall(targetX, targetZ, speed, swing, spin, type) {
            if (ballInPlay || state.matchOver) return;

            const from = new THREE.Vector3(0, 2.2, 12.5);
            const to = new THREE.Vector3(targetX, 0.3, targetZ);

            const dir = new THREE.Vector3().copy(to).sub(from);
            const dist = dir.length();
            dir.normalize();

            // Add swing (horizontal curve)
            const swingDir = new THREE.Vector3(swing, 0, 0);

            const vel = dir.clone().multiplyScalar(speed);
            vel.y += 0.8; // upward toss
            vel.x += swing * 0.3;

            ball.position.copy(from);
            ball.userData.velocity.copy(vel);
            ball.userData.swing = swing;
            ball.userData.spin = spin;
            ball.userData.type = type;
            ball.userData.releaseTime = Date.now();
            ball.userData.bounceCount = 0;
            ball.userData.seamMovement = (Math.random() - 0.5) * 0.15;

            ballInPlay = true;
            state.ballInPlay = true;
            state.deliveries++;
            state.balls++;

            document.getElementById('ball-indicator').classList.add('active');
            document.getElementById('ball-indicator').textContent = `โšก ${type} delivery!`;

            // Update bowler animation
            animateBowlerRunup();
        }

        function updateBallPhysics(delta) {
            if (!ballInPlay) return;

            const vel = ball.userData.velocity;
            const pos = ball.position;

            // Gravity
            vel.y += GRAVITY * delta;

            // Air resistance
            vel.multiplyScalar(AIR_RESISTANCE);

            // Swing (drift in air)
            if (ball.userData.swing && pos.y > 0.3) {
                vel.x += ball.userData.swing * delta * 0.5;
            }

            // Seam movement (random deviation on bounce)
            if (ball.userData.bounceCount === 0 && pos.y < 0.2 && vel.y < 0) {
                vel.x += ball.userData.seamMovement * 1.5;
                vel.z += (Math.random() - 0.5) * 0.1;
                ball.userData.bounceCount++;
            }

            // Spin effect after bounce
            if (ball.userData.spin && pos.y < 0.3) {
                const spinFactor = ball.userData.spin * delta * 2;
                vel.x += spinFactor * (pos.z > 0 ? -1 : 1);
                vel.z += spinFactor * 0.3;
            }

            // Update position
            pos.x += vel.x * delta;
            pos.y += vel.y * delta;
            pos.z += vel.z * delta;

            // Bounce off ground
            if (pos.y < 0.05) {
                pos.y = 0.05;
                if (vel.y < 0) {
                    vel.y *= -BOUNCE_COEFF;
                    ball.userData.bounceCount++;
                    // Reduce speed on bounce
                    vel.x *= 0.9;
                    vel.z *= 0.9;
                }
            }

            // Boundary check
            const radius = Math.sqrt(pos.x * pos.x + pos.z * pos.z);
            if (radius > 27) {
                // Boundary!
                if (pos.y > 0.5) {
                    // Six!
                    handleBoundary(true);
                } else {
                    // Four!
                    handleBoundary(false);
                }
                ballInPlay = false;
                state.ballInPlay = false;
                resetBall();
                return;
            }

            // Check if ball passed the stumps
            if (pos.z < -11.5 && ball.userData.bounceCount > 0) {
                // Check if batsman hit it
                const batPos = state.batsmanObj.position;
                const dx = pos.x - batPos.x;
                const dz = pos.z - batPos.z;
                const dist2 = Math.sqrt(dx * dx + dz * dz);

                if (dist2 < 0.8 && pos.y > 0.3 && pos.y < 1.8) {
                    // Hit!
                    handleBatContact(pos);
                } else if (pos.y < 0.4) {
                    // Ball passed through - check LBW or bowled
                    if (Math.abs(pos.x) < 0.3) {
                        // Bowled!
                        handleWicket('bowled');
                        ballInPlay = false;
                        state.ballInPlay = false;
                        resetBall();
                        return;
                    }
                    // Byes
                    handleByes();
                    ballInPlay = false;
                    state.ballInPlay = false;
                    resetBall();
                    return;
                }
            }

            // Ball goes too far
            if (pos.y < -2 || Math.abs(pos.x) > 40 || Math.abs(pos.z) > 40) {
                ballInPlay = false;
                state.ballInPlay = false;
                resetBall();
            }

            // Trail
            if (ballInPlay) {
                ball.userData.trail.push(pos.clone());
                if (ball.userData.trail.length > 30) ball.userData.trail.shift();
            }

            // Update ball rotation
            ball.rotation.x += vel.z * delta * 5;
            ball.rotation.z += vel.x * delta * 5;
        }

        // -------- BATTING --------
        let batSwing = 0;
        let isBatting = false;
        let shotResult = null;

        function handleBatContact(ballPos) {
            if (isBatting) return;
            isBatting = true;

            const power = state.power;
            const shotType = state.shotType;

            // Determine direction based on shot type
            let dx = 0,
                dz = -1;
            switch (shotType) {
                case 'drive':
                    dx = (ballPos.x - state.batsmanObj.position.x) * 0.5;
                    dz = -1.2;
                    break;
                case 'cover':
                    dx = -0.8;
                    dz = -1.2;
                    break;
                case 'pull':
                    dx = 0.6;
                    dz = -1.5;
                    break;
                case 'sweep':
                    dx = -0.3;
                    dz = -1.8;
                    break;
                case 'defend':
                    dx = 0;
                    dz = -0.3;
                    break;
                default:
                    dx = 0;
                    dz = -1;
            }

            const speed = 8 + power * 15;
            const angle = Math.atan2(dx, dz);

            // Edge chance
            const edgeChance = 0.15 - power * 0.08;
            if (Math.random() < edgeChance) {
                // Edge!
                const edgeDir = new THREE.Vector3(
                    (Math.random() - 0.5) * 2,
                    0.3 + Math.random() * 0.5,
                    -0.5 - Math.random() * 0.5
                ).normalize();
                ball.userData.velocity.copy(edgeDir.multiplyScalar(speed * 0.6));
                // Check for catch
                if (Math.random() < 0.4) {
                    handleWicket('caught');
                    showMessage('๐ŸŽฏ Caught!', 'Edged to the keeper!');
                } else {
                    // Runs from edge
                    const runs = Math.floor(Math.random() * 2) + 1;
                    addRuns(runs);
                    showMessage(`โœ๏ธ ${runs} runs`, 'Edge!');
                }
                isBatting = false;
                return;
            }

            // Normal hit
            const hitDir = new THREE.Vector3(
                Math.sin(angle) * 0.8 + (Math.random() - 0.5) * 0.2,
                0.3 + power * 0.6 + Math.random() * 0.2,
                -Math.cos(angle) * 0.8 - 0.3
            ).normalize();

            const hitSpeed = speed * (0.7 + power * 0.3);
            ball.userData.velocity.copy(hitDir.multiplyScalar(hitSpeed));
            ball.userData.velocity.y += 1 + power * 1.5;

            // Check for boundary
            const isSix = power > 0.7 && Math.random() < 0.3;
            if (isSix) {
                setTimeout(() => {
                    handleBoundary(true);
                    isBatting = false;
                }, 500);
            } else if (power > 0.5 && Math.random() < 0.2) {
                setTimeout(() => {
                    handleBoundary(false);
                    isBatting = false;
                }, 500);
            } else {
                // Running
                const runs = Math.floor(Math.random() * 2) + 1;
                if (Math.random() < 0.1 && runs === 2) {
                    // Run out chance
                    if (Math.random() < 0.3) {
                        handleWicket('runout');
                        showMessage('โŒ Run Out!', 'Direct hit!');
                    } else {
                        addRuns(runs);
                        showMessage(`๐Ÿƒ ${runs} runs`, 'Quick running!');
                    }
                } else {
                    addRuns(runs);
                    showMessage(`๐Ÿ ${runs} run${runs > 1 ? 's' : ''}`, 'Good running');
                }
                isBatting = false;
            }

            // Bat swing animation
            batSwing = 1.0;
            setTimeout(() => { isBatting = false; }, 800);

            // Update ball state
            ballInPlay = false;
            state.ballInPlay = false;
        }

        function handleBoundary(isSix) {
            if (isSix) {
                addRuns(6);
                state.sixes++;
                showMessage('๐ŸŽ‰ SIX!', 'Maximum!');
            } else {
                addRuns(4);
                state.boundary++;
                showMessage('๐Ÿ FOUR!', 'Races to the boundary!');
            }
            ballInPlay = false;
            state.ballInPlay = false;
            resetBall();
        }

        function handleByes() {
            const byes = Math.floor(Math.random() * 2) + 1;
            addRuns(byes);
            state.extras.bye += byes;
            showMessage(`๐Ÿ”„ ${byes} bye${byes > 1 ? 's' : ''}`, 'Missed the ball');
        }

        function handleWicket(type) {
            state.wickets++;
            if (type === 'bowled') {
                showMessage('๐Ÿ’ฅ Bowled!', 'Clean bowled!');
            } else if (type === 'caught') {
                showMessage('๐Ÿ–๏ธ Caught!', 'Out!');
            } else if (type === 'lbw') {
                showMessage('๐Ÿ“ LBW!', 'Plumb in front!');
            } else if (type === 'runout') {
                showMessage('โŒ Run Out!', 'Direct hit!');
            } else if (type === 'stumped') {
                showMessage('๐Ÿงค Stumped!', 'Quick hands!');
            }

            if (state.wickets >= 10) {
                endInnings();
            }

            updateScoreboard();
            setTimeout(resetBall, 1500);
        }

        function addRuns(runs) {
            state.runs += runs;
            state.totalRuns += runs;
            updateScoreboard();
        }

        function endInnings() {
            state.matchOver = true;
            state.inningsBreak = true;
            showMessage('๐Ÿ Innings Over!', `${state.team1} scored ${state.runs}/${state.wickets}`);
            document.getElementById('match-status').textContent = 'โธ๏ธ Innings Break';
            document.getElementById('btn-bowl').disabled = true;
            document.getElementById('btn-shot').disabled = true;
        }

        // -------- BOWLING --------
        let isBowlingAction = false;
        let bowlPower = 0;
        let isCharging = false;
        let chargeStartTime = 0;

        function animateBowlerRunup() {
            const bowler = state.bowlerObj;
            if (!bowler) return;

            // Simple run-up animation
            const startZ = 12.5;
            const endZ = 10.5;
            const duration = 300;

            let startTime = Date.now();

            function runupStep() {
                const elapsed = Date.now() - startTime;
                const progress = Math.min(elapsed / duration, 1);
                const ease = 1 - Math.pow(1 - progress, 3);
                bowler.position.z = startZ - (startZ - endZ) * ease;
                bowler.position.y = Math.sin(progress * Math.PI) * 0.15;

                if (progress < 1) {
                    requestAnimationFrame(runupStep);
                } else {
                    // Release ball
                    bowler.position.z = endZ;
                    bowler.position.y = 0;
                    // Trigger ball release
                    releaseBall();
                }
            }
            runupStep();
        }

        function releaseBall() {
            // Determine delivery type
            const types = ['fast', 'swing', 'seam', 'offspin', 'legspin'];
            const type = types[Math.floor(Math.random() * types.length)];

            let speed = 0,
                swing = 0,
                spin = 0,
                targetX = 0;

            switch (type) {
                case 'fast':
                    speed = 22 + Math.random() * 4;
                    swing = (Math.random() - 0.5) * 0.3;
                    targetX = (Math.random() - 0.5) * 0.6;
                    break;
                case 'swing':
                    speed = 18 + Math.random() * 3;
                    swing = (Math.random() - 0.5) * 0.8;
                    targetX = (Math.random() - 0.5) * 0.8;
                    break;
                case 'seam':
                    speed = 20 + Math.random() * 3;
                    swing = (Math.random() - 0.5) * 0.2;
                    targetX = (Math.random() - 0.5) * 0.4;
                    break;
                case 'offspin':
                    speed = 14 + Math.random() * 3;
                    spin = 0.5 + Math.random() * 0.5;
                    targetX = (Math.random() - 0.5) * 0.3;
                    break;
                case 'legspin':
                    speed = 13 + Math.random() * 3;
                    spin = -0.5 - Math.random() * 0.5;
                    targetX = (Math.random() - 0.5) * 0.3;
                    break;
                default:
                    speed = 18;
                    targetX = 0;
            }

            // Add some variation
            targetX += (Math.random() - 0.5) * 0.4;

            const targetZ = -11;
            state.deliveryType = type;
            state.power = 0.5 + Math.random() * 0.4;

            bowlBall(targetX, targetZ, speed, swing, spin, type);

            // Update UI
            document.getElementById('ball-indicator').textContent = `๐ŸŽฏ ${type.toUpperCase()} delivery!`;
        }

        // -------- FIELDING AI --------
        function updateFielders() {
            if (!ballInPlay) {
                // Return to positions
                state.fielderObjs.forEach(f => {
                    const home = f.userData.homePos;
                    f.position.x += (home.x - f.position.x) * 0.02;
                    f.position.z += (home.z - f.position.z) * 0.02;
                });
                return;
            }

            const ballPos = ball.position;
            state.fielderObjs.forEach(f => {
                const dx = ballPos.x - f.position.x;
                const dz = ballPos.z - f.position.z;
                const dist = Math.sqrt(dx * dx + dz * dz);

                if (dist < 10 && dist > 0.5) {
                    const speed = f.userData.speed * 3;
                    f.position.x += (dx / dist) * speed * 0.02;
                    f.position.z += (dz / dist) * speed * 0.02;
                    // Face ball
                    f.rotation.y = Math.atan2(dx, dz);
                }

                // Dive if close to ball
                if (dist < 0.6 && ballPos.y < 0.5) {
                    // Fielding!
                    if (ballPos.y < 0.3) {
                        // Stop ball
                        ball.userData.velocity.multiplyScalar(0.3);
                        // Throw to keeper
                        setTimeout(() => {
                            const keeper = state.wkObj;
                            if (keeper) {
                                const kdx = keeper.position.x - f.position.x;
                                const kdz = keeper.position.z - f.position.z;
                                const kdist = Math.sqrt(kdx * kdx + kdz * kdz);
                                if (kdist < 15) {
                                    // Throw animation
                                    const throwSpeed = 10;
                                    ball.userData.velocity.set(
                                        (kdx / kdist) * throwSpeed,
                                        1 + Math.random(),
                                        (kdz / kdist) * throwSpeed
                                    );
                                }
                            }
                        }, 100);
                    }
                }
            });
        }

        // -------- CAMERA --------
        let currentCamera = 'broadcast';
        const cameraTarget = new THREE.Vector3(0, 0, 0);

        function updateCamera() {
            const pos = camera.position;
            const target = cameraTarget;

            switch (currentCamera) {
                case 'broadcast':
                    pos.set(40, 28, 55);
                    target.set(0, 1, -2);
                    break;
                case 'bowler':
                    pos.set(0, 3, 18);
                    target.set(0, 0.5, -8);
                    break;
                case 'batsman':
                    pos.set(-2, 2, -12);
                    target.set(0, 1, 0);
                    break;
                case 'behind':
                    pos.set(0, 1.5, -14);
                    target.set(0, 0.5, 0);
                    break;
                case 'field':
                    pos.set(25, 18, 10);
                    target.set(0, 0.5, -2);
                    break;
                case 'ball':
                    if (ballInPlay) {
                        const bp = ball.position;
                        pos.set(bp.x + 5, bp.y + 4, bp.z + 8);
                        target.copy(bp);
                    } else {
                        pos.set(20, 15, 30);
                        target.set(0, 0.5, -2);
                    }
                    break;
                default:
                    pos.set(40, 28, 55);
                    target.set(0, 1, -2);
            }

            camera.lookAt(target);
        }

        // -------- UI UPDATES --------
        function updateScoreboard() {
            document.getElementById('team1-score').textContent = state.runs;
            document.getElementById('team1-wickets').textContent = `/${state.wickets}`;
            document.getElementById('team1-overs').textContent = `(${state.overs.toFixed(1)})`;

            // Update batsman/bowler names
            const batsmen = TEAMS[state.team1].players;
            const bowlers = TEAMS[state.team2].players;
            state.striker = batsmen[state.batsman % batsmen.length];
            state.currentBowler = bowlers[state.bowler % bowlers.length];
            document.getElementById('current-batsman').textContent = state.striker;
            document.getElementById('current-bowler').textContent = state.currentBowler;

            document.getElementById('match-overs').textContent = state.overs.toFixed(1);
            document.getElementById('match-runs').textContent = state.runs;
            document.getElementById('match-wickets').textContent = state.wickets;
            document.getElementById('match-balls').textContent = state.balls;
        }

        function showMessage(main, sub = '') {
            const el = document.getElementById('message');
            document.getElementById('msg-text').textContent = main;
            document.getElementById('msg-sub').textContent = sub;
            el.classList.add('active');
            clearTimeout(el._timeout);
            el._timeout = setTimeout(() => {
                el.classList.remove('active');
            }, 2500);
        }

        // -------- GAME LOOP --------
        let lastTime = 0;
        let bowlerTimer = 0;

        function gameLoop(time) {
            const delta = Math.min((time - lastTime) / 1000, 0.05);
            lastTime = time;

            // Update ball physics
            updateBallPhysics(delta);

            // Update fielders
            updateFielders();

            // Update camera
            updateCamera();

            // Update bowler timer
            if (!ballInPlay && !state.matchOver && !state.inningsBreak) {
                bowlerTimer += delta;
                if (bowlerTimer > 2.5) {
                    bowlerTimer = 0;
                    // Auto-bowl if not in batting mode
                    if (!state.isBatting) {
                        // Random delivery
                        const types = ['fast', 'swing', 'seam', 'offspin', 'legspin'];
                        const type = types[Math.floor(Math.random() * types.length)];
                        state.deliveryType = type;
                        const targetX = (Math.random() - 0.5) * 0.8;
                        const speed = 16 + Math.random() * 8;
                        const swing = (Math.random() - 0.5) * 0.6;
                        const spin = (Math.random() - 0.5) * 0.8;
                        bowlBall(targetX, -11, speed, swing, spin, type);
                        document.getElementById('ball-indicator').textContent = `๐ŸŽฏ ${type.toUpperCase()}!`;
                    }
                }
            }

            // Update batsman animation
            if (state.batsmanObj) {
                const b = state.batsmanObj;
                if (batSwing > 0) {
                    b.rotation.x = -0.3 + batSwing * 0.8;
                    batSwing *= 0.95;
                } else {
                    b.rotation.x = -0.1;
                }
                // Slight idle sway
                b.position.y = Math.sin(time * 0.001) * 0.008;
            }

            // Update bowler animation
            if (state.bowlerObj) {
                const b = state.bowlerObj;
                b.position.y = Math.sin(time * 0.0015) * 0.01;
            }

            renderer.render(scene, camera);
            requestAnimationFrame(gameLoop);
        }

        // -------- CONTROLS --------
        document.getElementById('btn-bowl').addEventListener('click', () => {
            if (ballInPlay || state.matchOver) return;
            const types = ['fast', 'swing', 'seam', 'offspin', 'legspin'];
            const type = types[Math.floor(Math.random() * types.length)];
            state.deliveryType = type;
            const targetX = (Math.random() - 0.5) * 0.8;
            const speed = 16 + Math.random() * 8;
            const swing = (Math.random() - 0.5) * 0.6;
            const spin = (Math.random() - 0.5) * 0.8;
            bowlBall(targetX, -11, speed, swing, spin, type);
            document.getElementById('ball-indicator').textContent = `๐ŸŽฏ ${type.toUpperCase()}!`;
        });

        document.getElementById('btn-shot').addEventListener('click', () => {
            if (!ballInPlay || isBatting) return;
            const shots = ['drive', 'cover', 'pull', 'sweep'];
            state.shotType = shots[Math.floor(Math.random() * shots.length)];
            state.power = 0.5 + Math.random() * 0.5;
            // Trigger shot on next ball contact
            // The ball physics will handle it
            showMessage('๐Ÿ Shot!', `Playing ${state.shotType}`);
        });

        document.getElementById('btn-defend').addEventListener('click', () => {
            if (!ballInPlay || isBatting) return;
            state.shotType = 'defend';
            state.power = 0.2 + Math.random() * 0.2;
            showMessage('๐Ÿ›ก๏ธ Defend', 'Playing safe');
        });

        document.getElementById('btn-leave').addEventListener('click', () => {
            if (!ballInPlay) return;
            // Leave the ball
            ball.userData.velocity.multiplyScalar(0.5);
            showMessage('๐Ÿšถ Leave', 'Let it go');
        });

        document.getElementById('btn-reset').addEventListener('click', () => {
            resetBall();
            state.runs = 0;
            state.wickets = 0;
            state.overs = 0;
            state.balls = 0;
            state.totalRuns = 0;
            state.totalWickets = 0;
            state.matchOver = false;
            state.inningsBreak = false;
            state.deliveries = 0;
            state.boundary = 0;
            state.sixes = 0;
            state.extras = { wide: 0, noBall: 0, bye: 0, legBye: 0 };
            document.getElementById('btn-bowl').disabled = false;
            document.getElementById('btn-shot').disabled = false;
            document.getElementById('match-status').textContent = 'โ— Live';
            document.getElementById('match-type').textContent = 'T20';
            updateScoreboard();
            showMessage('๐Ÿ”„ Match Reset', 'New innings');
            setupTeams();
        });

        document.getElementById('btn-toss').addEventListener('click', () => {
            const winner = Math.random() < 0.5 ? state.team1 : state.team2;
            const choice = Math.random() < 0.5 ? 'bat' : 'bowl';
            showMessage(`๐Ÿช™ ${winner} won the toss`, `Elect to ${choice} first`);
            if (winner === state.team1) {
                state.batting = state.team1;
                state.bowling = state.team2;
            } else {
                state.batting = state.team2;
                state.bowling = state.team1;
            }
        });

        // Camera switching
        document.querySelectorAll('#camera-selector button').forEach(btn => {
            btn.addEventListener('click', () => {
                document.querySelectorAll('#camera-selector button').forEach(b => b.classList.remove('active'));
                btn.classList.add('active');
                currentCamera = btn.dataset.cam;
                // Smooth transition
                camera.position.lerp(new THREE.Vector3(40, 28, 55), 0.5);
            });
        });

        // Keyboard shortcuts
        document.addEventListener('keydown', (e) => {
            if (e.key === 'b' || e.key === 'B') {
                document.getElementById('btn-bowl').click();
            }
            if (e.key === 's' || e.key === 'S') {
                document.getElementById('btn-shot').click();
            }
            if (e.key === 'd' || e.key === 'D') {
                document.getElementById('btn-defend').click();
            }
            if (e.key === 'l' || e.key === 'L') {
                document.getElementById('btn-leave').click();
            }
            if (e.key === 'r' || e.key === 'R') {
                document.getElementById('btn-reset').click();
            }
            if (e.key === 't' || e.key === 'T') {
                document.getElementById('btn-toss').click();
            }
            // Camera shortcuts
            if (e.key === '1') {
                document.querySelector('[data-cam="broadcast"]')?.click();
            }
            if (e.key === '2') {
                document.querySelector('[data-cam="bowler"]')?.click();
            }
            if (e.key === '3') {
                document.querySelector('[data-cam="batsman"]')?.click();
            }
            if (e.key === '4') {
                document.querySelector('[data-cam="behind"]')?.click();
            }
            if (e.key === '5') {
                document.querySelector('[data-cam="field"]')?.click();
            }
            if (e.key === '6') {
                document.querySelector('[data-cam="ball"]')?.click();
            }
        });

        // -------- RESIZE --------
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

        // -------- INIT --------
        setupTeams();
        updateScoreboard();
        showMessage('๐Ÿ Cricket Game', 'Press B to bowl, S to shot');
        document.getElementById('match-type').textContent = 'T20';

        // Start the game loop
        requestAnimationFrame(gameLoop);

        // Show power meter hint
        document.getElementById('power-meter').classList.add('active');

        console.log('๐Ÿ Cricket Game loaded!');
        console.log('Controls: B=Bowl, S=Shot, D=Defend, L=Leave, R=Reset, T=Toss');
        console.log('Camera: 1=Broadcast, 2=Bowler, 3=Batsman, 4=Behind, 5=Field, 6=Ball');

        // -------- SIMPLE BALL TRAIL (visual) --------
        // We'll use a simple line for the ball trail
        const trailMat = new THREE.LineBasicMaterial({ color: 0xffdd44, transparent: true, opacity: 0.3 });
        const trailGeo = new THREE.BufferGeometry();
        const trailPositions = new Float32Array(30 * 3);
        trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPositions, 3));
        const trailLine = new THREE.Line(trailGeo, trailMat);
        scene.add(trailLine);

        // Update trail in the loop
        setInterval(() => {
            if (ballInPlay && ball.userData.trail.length > 2) {
                const positions = trailLine.geometry.attributes.position.array;
                const trail = ball.userData.trail;
                const count = Math.min(trail.length, 30);
                for (let i = 0; i < count; i++) {
                    const idx = trail.length - count + i;
                    positions[i * 3] = trail[idx].x;
                    positions[i * 3 + 1] = trail[idx].y;
                    positions[i * 3 + 2] = trail[idx].z;
                }
                trailLine.geometry.attributes.position.needsUpdate = true;
                trailLine.geometry.setDrawRange(0, count);
                trailLine.visible = true;
            } else {
                trailLine.visible = false;
            }
        }, 50);

        // Also add the ball glow effect
        const glowMat = new THREE.MeshBasicMaterial({
            color: 0xff6633,
            transparent: true,
            opacity: 0.15
        });
        const glow = new THREE.Mesh(new THREE.SphereGeometry(0.15, 12, 12), glowMat);
        ball.add(glow);

        console.log('โœ… All systems ready!');
    </script>
</body>
</html>

Game Source: ๐Ÿ Realistic 3D Cricket Game

Creator: SuperWolf11

Libraries: three

Complexity: complex (1819 lines, 66.2 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: realistic-3d-cricket-game-superwolf11" to link back to the original. Then publish at arcadelab.ai/publish.