🎮ArcadeLab

Cricket Champions

by ShadowOwl70
950 lines32.1 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,maximum-scale=1,user-scalable=no">
<title>Cricket Champions</title>
<style>
:root{
  --gold:#ffc928; --blue:#071426; --panel:rgba(4,12,26,.9);
  --green:#22c55e; --red:#ef4444;
}
*{box-sizing:border-box}
body{
  margin:0; overflow:hidden; background:#020713; color:white;
  font-family:Inter,system-ui,Arial,sans-serif; touch-action:manipulation;
}
canvas{display:block}
button,select,input{font:inherit}
button{
  border:0; border-radius:9px; padding:10px 14px; cursor:pointer;
  color:white; background:#163257; font-weight:800;
}
button:hover{filter:brightness(1.2)}
button:active{transform:scale(.97)}
.gold{background:linear-gradient(135deg,#ffb300,#ff6f00); color:#151000}
.green{background:#15803d}
.red{background:#b91c1c}
.hidden{display:none!important}

#menu,#toss,#result,#pauseMenu{
  position:fixed; inset:0; z-index:20; display:flex;
  align-items:center; justify-content:center; padding:18px;
  background:radial-gradient(circle at top,rgba(18,62,109,.95),rgba(1,5,14,.98));
}
.card{
  width:min(720px,100%); max-height:94vh; overflow:auto;
  padding:24px; border-radius:20px; background:rgba(3,13,29,.94);
  border:1px solid #31527a; box-shadow:0 20px 80px #000;
}
h1{font-size:clamp(34px,7vw,66px); margin:0; color:var(--gold); line-height:.95}
h2,h3{margin:8px 0}
.subtitle{color:#9fc5ed; margin:10px 0 20px}
.modeGrid,.shopGrid{display:grid; grid-template-columns:repeat(2,1fr); gap:10px}
.mode{
  text-align:left; min-height:74px; background:linear-gradient(135deg,#102b4b,#08182d);
}
.mode small{display:block; color:#a8c4df; margin-top:4px}
.topline{display:flex; justify-content:space-between; gap:8px; flex-wrap:wrap}
.badge{background:#122842; padding:7px 10px; border-radius:20px; color:#d7ecff}

#hud{position:fixed; inset:0; z-index:5; pointer-events:none}
#scorebar{
  pointer-events:auto; margin:10px auto; width:min(970px,calc(100% - 20px));
  display:flex; justify-content:space-between; align-items:center; gap:8px;
  padding:9px 12px; border-radius:12px; background:var(--panel);
  border-bottom:3px solid var(--gold); box-shadow:0 5px 25px #0008;
}
#score{font-size:clamp(18px,4vw,30px); font-weight:900}
#detail{font-size:12px; color:#bcd5ed; text-align:center}
.iconbtn{padding:8px 11px}
#message{
  position:absolute; top:92px; left:50%; transform:translateX(-50%);
  min-width:220px; max-width:90%; padding:9px 16px; border-radius:10px;
  background:#061326dc; text-align:center; font-weight:900;
  transition:.25s; text-shadow:0 2px 3px #000;
}
#controls{
  pointer-events:auto; position:absolute; bottom:10px; left:50%;
  transform:translateX(-50%); width:min(900px,calc(100% - 16px));
  padding:10px; border-radius:15px; background:var(--panel);
}
#shots{display:grid; grid-template-columns:repeat(7,1fr); gap:6px}
.shot{padding:9px 4px; font-size:12px}
#runBtn{margin-top:7px; width:100%; background:#15803d}
#bowlControls{display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; align-items:end}
label{font-size:11px; color:#b7d0e8}
label>*{display:block; width:100%; margin-top:4px}
select,input{
  border:1px solid #31527a; background:#081a30; color:white;
  border-radius:7px; padding:8px;
}
#bowlBtn{grid-column:1/-1; background:#b91c1c}
#ticker{
  position:absolute; right:12px; top:100px; width:190px;
  background:#061326dd; border-radius:10px; padding:10px; font-size:12px;
}
#balls{display:flex; gap:4px; margin-top:7px; flex-wrap:wrap}
.ballMark{
  width:25px; height:25px; display:grid; place-items:center;
  border-radius:50%; background:#1e3a5f; font-size:10px; font-weight:bold;
}
#settingsBox{display:grid; gap:12px; margin:15px 0}
.statline{display:flex; justify-content:space-between; padding:7px; border-bottom:1px solid #234}
#toast{
  position:fixed; z-index:50; bottom:20px; left:50%; transform:translateX(-50%);
  background:#111d; padding:10px 18px; border-radius:20px; opacity:0;
  pointer-events:none; transition:.3s;
}

@media(max-width:650px){
  #shots{grid-template-columns:repeat(4,1fr)}
  #ticker{top:100px; right:7px; width:145px}
  #controls{bottom:4px}
  .shot{font-size:10px; padding:8px 2px}
  #bowlControls{grid-template-columns:1fr 1fr}
  #bowlBtn{grid-column:1/-1}
}
</style>
</head>
<body>

<div id="menu">
  <div class="card">
    <div class="topline">
      <div>
        <h1>CRICKET<br>CHAMPIONS</h1>
        <div class="subtitle">Take the pitch. Build your legacy.</div>
      </div>
      <div>
        <div class="badge">⭐ Level <span id="level">1</span></div>
        <div class="badge" style="margin-top:7px">🪙 <span id="coins">0</span></div>
      </div>
    </div>

    <h3>Select mode</h3>
    <div class="modeGrid">
      <button class="mode" data-mode="quick">⚡ Quick Match<small>2 overs · 3 wickets</small></button>
      <button class="mode" data-mode="t20">🏆 T20 Match<small>20 overs · 10 wickets</small></button>
      <button class="mode" data-mode="tournament">🥇 Tournament<small>Win matches and trophies</small></button>
      <button class="mode" data-mode="practice">🎯 Practice<small>Unlimited batting</small></button>
      <button class="mode" data-mode="challenge">🔥 Challenge<small>Chase 36 from 12 balls</small></button>
    </div>

    <h3 style="margin-top:18px">Career</h3>
    <div id="career"></div>

    <h3>Equipment</h3>
    <div class="shopGrid">
      <button id="batShop">Unlock Golden Bat · 100 🪙</button>
      <button id="jerseyShop">Unlock Red Jersey · 150 🪙</button>
    </div>
  </div>
</div>

<div id="toss" class="hidden">
  <div class="card" style="text-align:center;max-width:480px">
    <h2>Match Toss</h2>
    <p id="tossText">Call heads or tails.</p>
    <div id="callButtons">
      <button onclick="callToss('Heads')">Heads</button>
      <button onclick="callToss('Tails')">Tails</button>
    </div>
    <div id="choiceButtons" class="hidden">
      <button class="gold" onclick="chooseInnings(true)">Bat First</button>
      <button onclick="chooseInnings(false)">Bowl First</button>
    </div>
  </div>
</div>

<div id="hud" class="hidden">
  <div id="scorebar">
    <div>
      <div id="teamName">CHAMPIONS</div>
      <div id="score">0/0</div>
    </div>
    <div id="detail"></div>
    <button class="iconbtn" onclick="pauseGame()">Ⅱ</button>
  </div>

  <div id="message">Prepare for the next delivery</div>

  <div id="ticker">
    <b>THIS OVER</b>
    <div id="balls"></div>
    <hr style="border-color:#234">
    <div id="matchInfo"></div>
  </div>

  <div id="controls">
    <div id="batControls">
      <div id="shots">
        <button class="shot" data-shot="defensive">1 Defensive</button>
        <button class="shot" data-shot="straight">2 Straight</button>
        <button class="shot" data-shot="cover">3 Cover</button>
        <button class="shot" data-shot="pull">4 Pull</button>
        <button class="shot" data-shot="cut">5 Cut</button>
        <button class="shot" data-shot="lofted">6 Lofted</button>
        <button class="shot" data-shot="sweep">7 Sweep</button>
      </div>
      <button id="runBtn">RUN · R</button>
    </div>

    <div id="bowlControls" class="hidden">
      <label>Delivery
        <select id="bowlType">
          <option>Fast</option><option>Swing</option><option>Spin</option>
          <option>Yorker</option><option>Bouncer</option><option>Slower</option>
        </select>
      </label>
      <label>Line
        <input id="line" type="range" min="-100" max="100" value="0">
      </label>
      <label>Pace
        <input id="pace" type="range" min="40" max="100" value="80">
      </label>
      <button id="bowlBtn">BOWL</button>
    </div>
  </div>
</div>

<div id="pauseMenu" class="hidden">
  <div class="card" style="max-width:440px">
    <h2>Paused</h2>
    <div id="settingsBox">
      <label>Time
        <select id="timeSetting"><option value="day">Day</option><option value="night">Night</option></select>
      </label>
      <label>Weather
        <select id="weatherSetting"><option value="clear">Clear</option><option value="rain">Rain</option></select>
      </label>
      <label><input id="soundSetting" type="checkbox" checked> Sound enabled</label>
    </div>
    <button class="green" onclick="resumeGame()">Resume</button>
    <button class="red" onclick="quitGame()">Quit Match</button>
  </div>
</div>

<div id="result" class="hidden">
  <div class="card" style="max-width:530px;text-align:center">
    <h2 id="resultTitle">Victory!</h2>
    <p id="resultText"></p>
    <div id="rewards"></div>
    <button class="gold" onclick="restartMatch()">Play Again</button>
    <button onclick="quitGame()">Main Menu</button>
  </div>
</div>

<div id="toast"></div>

<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script>
"use strict";

/* ---------- DOM helpers ---------- */
const $ = id => document.getElementById(id);
const show = (id, yes=true) => $(id).classList.toggle("hidden", !yes);
const clamp = (v,a,b) => Math.max(a,Math.min(b,v));
const rand = (a,b) => a + Math.random()*(b-a);
const pick = a => a[Math.floor(Math.random()*a.length)];

/* ---------- Persistent career ---------- */
let career = JSON.parse(localStorage.getItem("ccCareer") || "null") || {
  xp:0, coins:0, matches:0, wins:0, sixes:0, fours:0,
  wickets:0, trophies:0, goldenBat:false, redJersey:false
};
function saveCareer(){
  localStorage.setItem("ccCareer",JSON.stringify(career));
  renderCareer();
}
function renderCareer(){
  $("level").textContent = Math.floor(career.xp/100)+1;
  $("coins").textContent = career.coins;
  $("career").innerHTML = `
    <div class="statline"><span>XP</span><b>${career.xp}</b></div>
    <div class="statline"><span>Matches / Wins</span><b>${career.matches} / ${career.wins}</b></div>
    <div class="statline"><span>Fours / Sixes</span><b>${career.fours} / ${career.sixes}</b></div>
    <div class="statline"><span>Wickets / Trophies</span><b>${career.wickets} / ${career.trophies}</b></div>`;
  $("batShop").textContent = career.goldenBat ? "✓ Golden Bat Equipped" : "Unlock Golden Bat · 100 🪙";
  $("jerseyShop").textContent = career.redJersey ? "✓ Red Jersey Equipped" : "Unlock Red Jersey · 150 🪙";
}
function purchase(item,cost){
  if(career[item]) return toast("Already unlocked");
  if(career.coins<cost) return toast("Not enough coins");
  career.coins-=cost; career[item]=true; saveCareer(); toast("Equipment unlocked!");
}
$("batShop").onclick=()=>purchase("goldenBat",100);
$("jerseyShop").onclick=()=>purchase("redJersey",150);
renderCareer();

/* ---------- Three.js scene ---------- */
let scene,camera,renderer,ball,bat,batsman,bowler;
let fielders=[], crowd=[], rain=[];
let lastTime=0, paused=false;

function init3D(){
  scene=new THREE.Scene();
  scene.background=new THREE.Color(0x79bced);
  scene.fog=new THREE.Fog(0x8cc9ed,45,100);

  camera=new THREE.PerspectiveCamera(55,innerWidth/innerHeight,.1,180);
  camera.position.set(0,8,18);
  camera.lookAt(0,1,0);

  renderer=new THREE.WebGLRenderer({antialias:true});
  renderer.setPixelRatio(Math.min(devicePixelRatio,1.6));
  renderer.setSize(innerWidth,innerHeight);
  renderer.shadowMap.enabled=true;
  document.body.prepend(renderer.domElement);

  scene.add(new THREE.HemisphereLight(0xd9eeff,0x28461f,2.1));
  const sun=new THREE.DirectionalLight(0xffffff,2.3);
  sun.position.set(-20,35,15); sun.castShadow=true; scene.add(sun);

  // Oval field
  const grass=new THREE.Mesh(
    new THREE.CircleGeometry(43,80),
    new THREE.MeshStandardMaterial({color:0x238139,roughness:.95})
  );
  grass.rotation.x=-Math.PI/2; grass.scale.z=.78; grass.receiveShadow=true;
  scene.add(grass);

  // Boundary rope
  const rope=new THREE.Mesh(
    new THREE.TorusGeometry(38,.16,8,100),
    new THREE.MeshStandardMaterial({color:0xffffff})
  );
  rope.rotation.x=Math.PI/2; rope.scale.y=.78; rope.position.y=.13; scene.add(rope);

  // Pitch
  const pitch=new THREE.Mesh(
    new THREE.BoxGeometry(3.6,.12,22),
    new THREE.MeshStandardMaterial({color:0xc9a66b,roughness:1})
  );
  pitch.position.y=.09; pitch.receiveShadow=true; scene.add(pitch);

  // Crease lines
  [-8,8].forEach(z=>{
    const line=new THREE.Mesh(
      new THREE.BoxGeometry(4.2,.02,.1),
      new THREE.MeshBasicMaterial({color:0xffffff})
    );
    line.position.set(0,.17,z); scene.add(line);
  });

  createWickets(-8); createWickets(8);
  createStadium();
  createPlayers();

  ball=new THREE.Mesh(
    new THREE.SphereGeometry(.18,18,18),
    new THREE.MeshStandardMaterial({color:0xb51d28,roughness:.45})
  );
  ball.castShadow=true; ball.position.set(0,.35,-7); scene.add(ball);

  window.addEventListener("resize",()=>{
    camera.aspect=innerWidth/innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth,innerHeight);
  });
}
function createWickets(z){
  const mat=new THREE.MeshStandardMaterial({color:0xf4d78b});
  [-.35,0,.35].forEach(x=>{
    const stump=new THREE.Mesh(new THREE.CylinderGeometry(.045,.045,1.4,8),mat);
    stump.position.set(x,.75,z); scene.add(stump);
  });
}
function person(color,x,z,scale=1){
  const group=new THREE.Group();
  const body=new THREE.Mesh(
    new THREE.CapsuleGeometry(.28*scale,.75*scale,4,8),
    new THREE.MeshStandardMaterial({color})
  );
  body.position.y=1.05*scale; body.castShadow=true; group.add(body);
  const head=new THREE.Mesh(
    new THREE.SphereGeometry(.21*scale,12,12),
    new THREE.MeshStandardMaterial({color:0xb87955})
  );
  head.position.y=1.78*scale; group.add(head);
  group.position.set(x,0,z); scene.add(group); return group;
}
function createPlayers(){
  batsman=person(career.redJersey?0xc51b2f:0x1557b0,0,7.5);
  bowler=person(0xb91c1c,0,-7);
  bat=new THREE.Mesh(
    new THREE.BoxGeometry(.18,1.45,.12),
    new THREE.MeshStandardMaterial({color:career.goldenBat?0xffc400:0xd7a85b})
  );
  bat.position.set(.55,1.05,7.35); bat.rotation.z=-.25; scene.add(bat);

  [[-8,2],[8,1],[-12,-4],[12,-5],[0,-18],[-20,8],[20,9],[0,17]].forEach(p=>{
    fielders.push(person(0xb91c1c,p[0],p[1],.9));
  });
}
function createStadium(){
  const standMat=new THREE.MeshStandardMaterial({color:0x30445b});
  const stand=new THREE.Mesh(new THREE.TorusGeometry(46,5,8,80),standMat);
  stand.rotation.x=Math.PI/2; stand.scale.y=.78; stand.position.y=1.8; scene.add(stand);

  for(let i=0;i<150;i++){
    const a=Math.random()*Math.PI*2, r=43+Math.random()*5;
    const c=new THREE.Mesh(
      new THREE.BoxGeometry(.22,.35,.22),
      new THREE.MeshBasicMaterial({color:pick([0xffd43b,0x2585e6,0xe63d51,0xffffff])})
    );
    c.position.set(Math.cos(a)*r,3+Math.random()*2,Math.sin(a)*r*.78);
    scene.add(c); crowd.push(c);
  }

  // Dressing rooms
  [-1,1].forEach(s=>{
    const room=new THREE.Mesh(
      new THREE.BoxGeometry(12,4,5),
      new THREE.MeshStandardMaterial({color:0x17273b})
    );
    room.position.set(s*28,2,-23); scene.add(room);
  });

  // Scoreboard
  const board=new THREE.Mesh(
    new THREE.BoxGeometry(13,6,.6),
    new THREE.MeshStandardMaterial({color:0x07111f,emissive:0x061629})
  );
  board.position.set(0,7,-31); scene.add(board);

  // Floodlights
  [[-30,-24],[30,-24],[-32,23],[32,23]].forEach(([x,z])=>{
    const pole=new THREE.Mesh(
      new THREE.CylinderGeometry(.13,.18,18,8),
      new THREE.MeshStandardMaterial({color:0x88939d})
    );
    pole.position.set(x,9,z); scene.add(pole);
    const lamp=new THREE.PointLight(0xffffff,0,55);
    lamp.position.set(x,18,z); lamp.userData.flood=true; scene.add(lamp);
  });
}

/* ---------- Match state ---------- */
const modeRules={
  quick:{overs:2,wickets:3},
  t20:{overs:20,wickets:10},
  tournament:{overs:3,wickets:5},
  practice:{overs:999,wickets:999},
  challenge:{overs:2,wickets:3}
};
let game={
  mode:"quick", overs:2, maxWickets:3, innings:0, userBatFirst:true,
  runs:0,wickets:0,balls:0,target:null,firstScore:null,
  striker:0,batterRuns:[0,0],bowlerRuns:0,bowlerWickets:0,
  overEvents:[], active:false, phase:"idle", delivery:null,
  shotUsed:false,runIntent:false,lastResult:"", tossWon:false
};

function startMode(mode){
  game.mode=mode;
  Object.assign(game,modeRules[mode]);
  show("menu",false);

  if(mode==="practice"){
    game.userBatFirst=true;
    beginMatch();
  }else if(mode==="challenge"){
    game.userBatFirst=false;
    game.innings=1;
    game.firstScore=35;
    game.target=36;
    beginInnings(true);
  }else{
    show("toss",true);
    $("tossText").textContent="Call heads or tails.";
    show("callButtons",true); show("choiceButtons",false);
  }
}
document.querySelectorAll("[data-mode]").forEach(b=>b.onclick=()=>startMode(b.dataset.mode));

function callToss(call){
  const coin=Math.random()<.5?"Heads":"Tails";
  game.tossWon=call===coin;
  show("callButtons",false);
  if(game.tossWon){
    $("tossText").textContent=`${coin}! You won the toss.`;
    show("choiceButtons",true);
  }else{
    const aiBats=Math.random()<.55;
    game.userBatFirst=!aiBats;
    $("tossText").textContent=`${coin}. Opponent chooses to ${aiBats?"bat":"bowl"} first.`;
    setTimeout(beginMatch,1500);
  }
}
function chooseInnings(bat){
  game.userBatFirst=bat;
  beginMatch();
}
function beginMatch(){
  show("toss",false); show("result",false); show("hud",true);
  game.innings=0; game.firstScore=null; game.target=null;
  beginInnings(game.userBatFirst);
}
function beginInnings(userBatting){
  game.runs=0; game.wickets=0; game.balls=0; game.striker=0;
  game.batterRuns=[0,0]; game.bowlerRuns=0; game.bowlerWickets=0;
  game.overEvents=[]; game.active=true; game.phase="idle";
  game.userBatting=userBatting;
  show("toss",false); show("result",false); show("hud",true);
  show("batControls",userBatting); show("bowlControls",!userBatting);
  resetCamera();
  updateHUD();
  say(userBatting ? "You are batting" : "You are bowling");
  if(userBatting) setTimeout(aiDeliver,1000);
}
function resetCamera(){
  camera.position.set(game.userBatting?0:0,7,game.userBatting?18:-18);
  camera.lookAt(0,1,0);
}

/* ---------- Batting ---------- */
function aiDeliver(){
  if(!game.active || paused || !game.userBatting || game.phase!=="idle") return;
  const types=["Fast","Swing","Spin","Yorker","Bouncer","Slower"];
  let type=pick(types);

  // Match-aware bowling strategy
  if(game.target && game.target-game.runs<10) type=pick(["Yorker","Slower","Fast"]);

  game.delivery={
    type,
    duration:type==="Fast"||type==="Bouncer"?850:type==="Slower"?1250:1050,
    start:performance.now(),
    line:rand(-.7,.7),
    swing:type==="Swing"?rand(-.8,.8):type==="Spin"?rand(-.45,.45):0
  };
  game.phase="delivery"; game.shotUsed=false; game.runIntent=false;
  ball.position.set(game.delivery.line,.35,-7);
  say(`${type} delivery`);
}
const shotData={
  defensive:{power:.25,risk:.03,angle:0},
  straight:{power:.68,risk:.10,angle:0},
  cover:{power:.72,risk:.13,angle:-.65},
  pull:{power:.78,risk:.17,angle:.85},
  cut:{power:.69,risk:.14,angle:-1.05},
  lofted:{power:1,risk:.34,angle:rand(-.4,.4)},
  sweep:{power:.68,risk:.18,angle:1.15}
};
function playShot(name){
  if(!game.active || !game.userBatting || game.phase!=="delivery" || game.shotUsed) return;
  game.shotUsed=true;
  const idealZ=6.35;
  const timing=Math.abs(ball.position.z-idealZ);
  const quality=clamp(1-timing/5,0,1);
  swingBat(name);

  if(timing>4.1){
    say("Missed!");
    return;
  }

  const d=game.delivery.type, data=shotData[name];
  let suitability=1;
  if(d==="Bouncer" && name==="pull") suitability=1.3;
  if(d==="Spin" && name==="sweep") suitability=1.3;
  if(d==="Yorker" && name==="straight") suitability=1.15;
  if(d==="Bouncer" && name==="defensive") suitability=.65;
  if(d==="Yorker" && name==="pull") suitability=.5;

  let power=quality*data.power*suitability*rand(.88,1.12);
  if(career.goldenBat) power*=1.08;

  game.phase="hit";
  const distance=8+power*38;
  const angle=data.angle+rand(-.25,.25);
  const loft=name==="lofted" ? 7+power*8 : 1+power*4;
  animateHit(
    new THREE.Vector3(Math.sin(angle)*distance,loft,7-Math.cos(angle)*distance),
    power,name,data.risk,quality
  );
}
function swingBat(name){
  const old=bat.rotation.z;
  bat.rotation.z=name==="sweep"?-1.6:-2.2;
  setTimeout(()=>bat.rotation.z=old,240);
  sound("hit");
}
function animateHit(target,power,name,risk,quality){
  const start=ball.position.clone(), begun=performance.now();
  const duration=950;
  function move(now){
    if(game.phase!=="hit") return;
    const t=clamp((now-begun)/duration,0,1);
    ball.position.lerpVectors(start,target,t);
    ball.position.y=Math.max(.2,(1-t)*start.y+t*target.y+Math.sin(t*Math.PI)*4*power);
    camera.position.x+=(ball.position.x-camera.position.x)*.025;
    camera.lookAt(ball.position);
    if(t<1) requestAnimationFrame(move);
    else resolveHit(power,name,risk,quality);
  }
  requestAnimationFrame(move);
}
function resolveHit(power,name,risk,quality){
  let wicket=false, runs=0, text="";
  const catchChance=risk*(1-quality*.55);

  if(Math.random()<catchChance){
    wicket=true; text=Math.random()<.7?"CAUGHT!":"BOWLED!";
  }else if(power>.83){
    runs=6; text="SIX!";
  }else if(power>.58){
    runs=4; text="FOUR!";
  }else if(power>.36){
    runs=Math.random()<.45?3:2; text=`${runs} runs`;
  }else if(power>.16){
    runs=1; text="Single";
  }else text="Dot ball";

  if(game.runIntent && !wicket && runs<4){
    if(Math.random()<.16+Math.max(0,.35-quality)){
      wicket=true; runs=0; text="RUN OUT!";
    }else{
      runs=Math.min(3,runs+1); text=`Quick running: ${runs}`;
    }
  }

  addBall(runs,wicket,false,text);
}

/* ---------- Bowling ---------- */
$("bowlBtn").onclick=()=>{
  if(!game.active || game.userBatting || game.phase!=="idle") return;
  const type=$("bowlType").value;
  const line=+$("line").value/100;
  const pace=+$("pace").value;
  game.delivery={type,line,pace,start:performance.now(),duration:1450-pace*7};
  game.phase="bowling";
  ball.position.set(0,.4,7);
  say(`${type} ball released`);

  const start=ball.position.clone();
  const end=new THREE.Vector3(line*1.7,.25,-7);
  const begun=performance.now();

  function move(now){
    if(game.phase!=="bowling") return;
    const t=clamp((now-begun)/game.delivery.duration,0,1);
    ball.position.lerpVectors(start,end,t);
    if(type==="Bouncer") ball.position.y=.3+Math.sin(t*Math.PI)*1.2;
    if(type==="Swing") ball.position.x+=Math.sin(t*Math.PI)*.55;
    if(type==="Spin") ball.rotation.y+=.35;
    if(t<1) requestAnimationFrame(move);
    else resolveAIBatting(type,line,pace);
  }
  requestAnimationFrame(move);
};
function resolveAIBatting(type,line,pace){
  if(Math.abs(line)>.88){
    addBall(1,false,true,"Wide");
    return;
  }

  let wicketChance=.08, boundary=.20;
  if(type==="Yorker" && Math.abs(line)<.25) wicketChance+=.13;
  if(type==="Swing" && Math.abs(line)<.45) wicketChance+=.07;
  if(type==="Slower" && pace<65) wicketChance+=.07;
  if(type==="Bouncer" && pace>85) wicketChance+=.06;
  if(type==="Fast" && pace>92) boundary+=.09;

  // AI attacks more aggressively while chasing
  if(game.target){
    const needed=game.target-game.runs;
    const ballsLeft=game.overs*6-game.balls;
    if(needed>ballsLeft*1.5){ boundary+=.17; wicketChance+=.08; }
  }

  const r=Math.random();
  if(r<wicketChance){
    career.wickets++; saveCareer();
    addBall(0,true,false,pick(["BOWLED!","CAUGHT!","LBW!"]));
  }else if(r<wicketChance+boundary){
    const runs=Math.random()<.72?4:6;
    addBall(runs,false,false,`AI hits ${runs}!`);
  }else{
    const runs=pick([0,0,1,1,1,2,2,3]);
    addBall(runs,false,false,runs?`${runs} run${runs>1?"s":""}`:"Dot ball");
  }
}

/* ---------- Scoring and innings ---------- */
function addBall(runs,wicket,extra,text){
  if(!game.active) return;

  game.runs+=runs;
  if(game.userBatting) game.batterRuns[game.striker]+=runs;
  else game.bowlerRuns+=runs;

  if(wicket){
    game.wickets++;
    if(!game.userBatting) game.bowlerWickets++;
  }

  if(!extra){
    game.balls++;
    game.overEvents.push(wicket?"W":String(runs));
    if(runs%2===1) game.striker=1-game.striker;
    if(game.balls%6===0){
      game.striker=1-game.striker;
      setTimeout(()=>game.overEvents=[],700);
    }
  }else{
    game.overEvents.push("Wd");
  }

  if(game.userBatting){
    if(runs===4) career.fours++;
    if(runs===6) career.sixes++;
  }

  sound(wicket?"wicket":runs>=4?"cheer":"bounce");
  say(text);
  updateHUD();

  if(checkEnd()) return;
  game.phase="waiting";

  setTimeout(()=>{
    if(!game.active) return;
    game.phase="idle";
    ball.position.set(0,.35,game.userBatting?-7:7);
    resetCamera();
    updateHUD();
    if(game.userBatting) setTimeout(aiDeliver,500);
  },1150);
}
function checkEnd(){
  if(game.mode==="practice") return false;

  const exhausted=game.balls>=game.overs*6 || game.wickets>=game.maxWickets;
  const chased=game.innings===1 && game.runs>=game.target;

  if(chased){
    finishMatch(game.userBatting);
    return true;
  }
  if(!exhausted) return false;

  if(game.innings===0){
    game.firstScore=game.runs;
    game.target=game.runs+1;
    game.innings=1;
    game.active=false;
    say(`Innings complete · Target ${game.target}`);
    setTimeout(()=>beginInnings(!game.userBatFirst),1800);
  }else{
    if(game.runs===game.firstScore) finishMatch(null);
    else finishMatch(game.runs>game.firstScore ? game.userBatting : !game.userBatting);
  }
  return true;
}
function finishMatch(userWon){
  game.active=false; game.phase="ended";
  career.matches++;

  let title,text,xp,coins;
  if(userWon===null){
    title="Match Tied";
    text=`Both teams scored ${game.runs}.`;
    xp=20; coins=10;
  }else if(userWon){
    title="Victory!";
    text=game.userBatting
      ? `Target chased with ${game.overs*6-game.balls} balls remaining.`
      : `You defended the total by ${game.firstScore-game.runs} runs.`;
    xp=50; coins=30; career.wins++;
    if(game.mode==="tournament") career.trophies++;
  }else{
    title="Defeat";
    text=game.userBatting
      ? `You fell ${game.target-game.runs} runs short.`
      : "The opponent completed the chase.";
    xp=15; coins=8;
  }

  career.xp+=xp; career.coins+=coins; saveCareer();
  $("resultTitle").textContent=title;
  $("resultText").textContent=text;
  $("rewards").innerHTML=`<h3>+${xp} XP &nbsp; +${coins} 🪙</h3>`;
  setTimeout(()=>show("result",true),700);
}
function updateHUD(){
  const overs=`${Math.floor(game.balls/6)}.${game.balls%6}`;
  $("score").textContent=`${game.runs}/${game.wickets}`;
  $("teamName").textContent=game.userBatting?"CHAMPIONS":"RIVALS";

  let detail=`Overs ${overs} / ${game.overs}`;
  if(game.userBatting)
    detail+=` · Batter ${game.batterRuns[game.striker]}* · Non-striker ${game.batterRuns[1-game.striker]}`;
  else
    detail+=` · Bowler ${game.bowlerWickets}/${game.bowlerRuns}`;

  if(game.target){
    const need=Math.max(0,game.target-game.runs);
    const left=Math.max(0,game.overs*6-game.balls);
    const rrr=left?((need/left)*6).toFixed(2):"∞";
    detail+=` · Need ${need} from ${left} · RRR ${rrr}`;
  }
  $("detail").textContent=detail;

  $("balls").innerHTML=game.overEvents.slice(-8)
    .map(x=>`<span class="ballMark">${x}</span>`).join("");

  $("matchInfo").innerHTML=`
    <div>Innings: ${game.innings+1}</div>
    <div>Mode: ${game.mode.toUpperCase()}</div>
    ${game.target?`<div>Target: ${game.target}</div>`:""}
    <div>Weather: ${$("weatherSetting").value}</div>`;
}

/* ---------- Input ---------- */
document.querySelectorAll("[data-shot]").forEach(b=>b.onclick=()=>playShot(b.dataset.shot));
$("runBtn").onclick=()=>{
  if(game.phase==="hit" || game.phase==="delivery"){
    game.runIntent=true; say("Run called!");
  }
};
addEventListener("keydown",e=>{
  const keys={
    "1":"defensive","2":"straight","3":"cover","4":"pull",
    "5":"cut","6":"lofted","7":"sweep"
  };
  if(keys[e.key]) playShot(keys[e.key]);
  if(e.key.toLowerCase()==="r") $("runBtn").click();
  if(e.key==="Escape") paused?resumeGame():pauseGame();
});

/* Swipe batting */
let touchStart=null;
addEventListener("touchstart",e=>{
  const t=e.changedTouches[0]; touchStart={x:t.clientX,y:t.clientY,time:Date.now()};
},{passive:true});
addEventListener("touchend",e=>{
  if(!touchStart || !game.userBatting) return;
  const t=e.changedTouches[0], dx=t.clientX-touchStart.x, dy=t.clientY-touchStart.y;
  if(Math.hypot(dx,dy)<35) return;
  if(dy<-50) playShot("lofted");
  else if(dy>50) playShot("defensive");
  else if(dx>0) playShot("pull");
  else playShot("cover");
},{passive:true});

/* ---------- Audio ---------- */
let audioCtx;
function sound(type){
  if(!$("soundSetting").checked) return;
  audioCtx ||= new (window.AudioContext||window.webkitAudioContext)();
  const o=audioCtx.createOscillator(), g=audioCtx.createGain();
  o.connect(g); g.connect(audioCtx.destination);
  const now=audioCtx.currentTime;
  const freq={hit:170,bounce:90,cheer:440,wicket:115}[type]||120;
  o.frequency.setValueAtTime(freq,now);
  if(type==="cheer") o.frequency.exponentialRampToValueAtTime(760,now+.45);
  g.gain.setValueAtTime(.12,now);
  g.gain.exponentialRampToValueAtTime(.001,now+(type==="cheer"?.65:.18));
  o.start(now); o.stop(now+(type==="cheer"?.7:.2));
}

/* ---------- Settings and menus ---------- */
function pauseGame(){
  if(!game.active) return;
  paused=true; show("pauseMenu",true);
}
function resumeGame(){
  paused=false; show("pauseMenu",false);
  if(game.userBatting && game.phase==="idle") aiDeliver();
}
function quitGame(){
  game.active=false; paused=false;
  show("pauseMenu",false); show("result",false); show("hud",false);
  show("toss",false); show("menu",true);
}
function restartMatch(){
  show("result",false);
  startMode(game.mode);
}
$("timeSetting").onchange=applyEnvironment;
$("weatherSetting").onchange=applyEnvironment;
function applyEnvironment(){
  const night=$("timeSetting").value==="night";
  scene.background.set(night?0x030a18:0x79bced);
  scene.fog.color.set(night?0x081126:0x8cc9ed);
  scene.traverse(o=>{
    if(o.userData.flood) o.intensity=night?35:0;
  });

  rain.forEach(r=>scene.remove(r)); rain=[];
  if($("weatherSetting").value==="rain"){
    for(let i=0;i<170;i++){
      const drop=new THREE.Mesh(
        new THREE.BoxGeometry(.018,.35,.018),
        new THREE.MeshBasicMaterial({color:0xaadfff})
      );
      drop.position.set(rand(-40,40),rand(2,24),rand(-35,35));
      scene.add(drop); rain.push(drop);
    }
  }
  updateHUD();
}
function say(text){
  $("message").textContent=text;
}
function toast(text){
  $("toast").textContent=text; $("toast").style.opacity=1;
  setTimeout(()=>$("toast").style.opacity=0,1500);
}

/* ---------- Animation loop ---------- */
function animate(now){
  requestAnimationFrame(animate);
  const dt=Math.min(.04,(now-lastTime)/1000||0); lastTime=now;

  if(!paused){
    crowd.forEach((c,i)=>c.position.y+=Math.sin(now*.004+i)*.0015);

    rain.forEach(r=>{
      r.position.y-=20*dt;
      r.position.x-=3*dt;
      if(r.position.y<0) r.position.y=24;
    });

    fielders.forEach((f,i)=>{
      f.rotation.y=Math.sin(now*.001+i)*.12;
    });

    if(game.phase==="delivery" && game.delivery){
      const d=game.delivery;
      const t=clamp((now-d.start)/d.duration,0,1);
      ball.position.z=-7+t*13.8;
      ball.position.x=d.line+Math.sin(t*Math.PI)*d.swing;
      ball.position.y=.25;

      if(d.type==="Bouncer") ball.position.y=.3+Math.sin(t*Math.PI)*1.1;
      if(d.type==="Spin") ball.rotation.y+=.25;

      if(t>=1){
        if(!game.shotUsed){
          const wicketChance=d.type==="Yorker"?.28:d.type==="Swing"?.18:.12;
          addBall(0,Math.random()<wicketChance,false,
            Math.random()<wicketChance?"BOWLED!":"Dot ball");
        }else if(game.phase==="delivery"){
          addBall(0,false,false,"Dot ball");
        }
      }
    }
  }
  renderer.render(scene,camera);
}

init3D();
applyEnvironment();
requestAnimationFrame(animate);
</script>
</body>
<

Game Source: Cricket Champions

Creator: ShadowOwl70

Libraries: three

Complexity: complex (950 lines, 32.1 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: cricket-champions-shadowowl70" to link back to the original. Then publish at arcadelab.ai/publish.