🎮ArcadeLab

Hill Climb Adventure 2

by BlazeGalaxy19
534 lines15.2 KB
▶ Play

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Hill Climb Adventure 2</title>
<style>
*{box-sizing:border-box}
body{margin:0;overflow:hidden;background:#76c9ef;font-family:Arial}
canvas{width:100vw;height:100vh;display:block}
#hud{position:absolute;top:12px;left:12px;right:12px;color:white;
display:flex;justify-content:space-between;gap:8px;
font-weight:bold;text-shadow:1px 2px 3px #222;pointer-events:none}
.stat{background:#172d36bd;padding:9px;border-radius:10px;min-width:90px}
.bar{height:9px;background:#444;border-radius:8px;overflow:hidden;margin-top:5px}
.fill{height:100%;width:100%;background:#39df69}
#rpmFill{background:#ffc23e}
#boostFill{background:#39d9ff}
#controls{position:absolute;bottom:15px;left:12px;right:12px;
display:flex;justify-content:space-between;align-items:end}
.pad{display:flex;gap:9px}
button{border:0;border-radius:12px;padding:12px 17px;font-weight:bold;cursor:pointer}
.control{width:68px;height:62px;background:#152d3ddd;color:white;
border:2px solid #ffffff9c;font-size:23px;touch-action:none}
#boost{background:#1689d5}
.panel{position:absolute;inset:0;background:#10242aeb;color:white;
display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:13px;text-align:center;padding:16px}
.panel h1{color:#ffce46}
.hidden{display:none!important}
.vehicle{display:flex;justify-content:space-between;gap:8px;
background:#29434c;padding:10px;margin:5px;border-radius:8px}
#garage{width:96%;max-width:500px;max-height:85vh;overflow:auto}
</style>
</head>
<body>
<canvas id="game"></canvas>

<div id="hud" class="hidden">
 <div>
  <div class="stat">🪙 <span id="coins">0</span></div>
  <div class="stat">💎 <span id="diamonds">0</span></div>
  <div class="stat">⛽ <span id="fuel">100</span>%</div>
 </div>
 <div>
  <div class="stat">⚡ BOOST <span id="boostText">100</span>%
   <div class="bar"><div id="boostFill" class="fill"></div></div>
  </div>
  <div class="stat">RPM <span id="rpm">0</span>
   <div class="bar"><div id="rpmFill" class="fill"></div></div>
  </div>
 </div>
 <div class="stat">📏 <span id="distance">0</span> m</div>
</div>

<div id="controls" class="hidden">
 <div class="pad">
  <button class="control" id="brake">◀</button>
  <button class="control" id="gas">▶</button>
 </div>
 <button class="control" id="boost">⚡</button>
</div>

<div id="menu" class="panel">
 <h1>🏔️ HILL CLIMB ADVENTURE</h1>
 <h3>Drive • Collect • Upgrade • Explore</h3>
 <button onclick="start()">PLAY</button>
 <button onclick="garageOpen()">VEHICLE GARAGE</button>
 <p>Arrow keys / WASD to drive</p>
 <p>Space to boost</p>
</div>

<div id="garage" class="panel hidden">
 <h1>🚘 GARAGE</h1>
 <div id="carList"></div>
 <button onclick="garageClose()">BACK</button>
</div>

<div id="gameOver" class="panel hidden">
 <h1>GAME OVER</h1>
 <h2 id="finalDistance"></h2>
 <button onclick="start()">PLAY AGAIN</button>
 <button onclick="home()">HOME</button>
</div>

<script>
const canvas=document.getElementById("game");
const ctx=canvas.getContext("2d");
let W=innerWidth,H=innerHeight,DPR=1;

function resize(){
 DPR=Math.min(devicePixelRatio||1,2);
 W=innerWidth;H=innerHeight;
 canvas.width=W*DPR;canvas.height=H*DPR;
 ctx.setTransform(DPR,0,0,DPR,0,0);
}
addEventListener("resize",resize);resize();

const cars=[
 {name:"Bicycle",price:0,emoji:"🚲",power:300,mass:1.0,r:13},
 {name:"Skateboard",price:0,emoji:"🛹",power:280,mass:.8,r:12},
 {name:"Motorbike",price:120,emoji:"🏍️",power:460,mass:1.1,r:19},
 {name:"Car",price:0,emoji:"🚗",power:420,mass:1.5,r:22},
 {name:"Tractor",price:250,emoji:"🚜",power:350,mass:2.2,r:27},
 {name:"Truck",price:400,emoji:"🚚",power:490,mass:2.7,r:29},
 {name:"JCB",price:550,emoji:"🚧",power:370,mass:2.8,r:26},
 {name:"Jet Ski",price:700,emoji:"🌊",power:550,mass:1.2,r:21},
 {name:"Buggy",price:900,emoji:"🏎️",power:570,mass:1.4,r:23},
 {name:"Monster Truck",price:1300,emoji:"🚙",power:620,mass:3,r:34}
];

let owned=[0,1,3],selected=3;
let coins=0,diamonds=0,best=0;
let playing=false;
let x=100,y=200,vx=0,vy=0,angle=0;
let camX=0,camY=0;
let fuel=100,boostPower=100,rpm=0;
let gas=false,brake=false,boost=false;
let objects=[],particles=[];
let grounded=false,worldDistance=0;

const GRAVITY=1250;
const AXLE=64;
const WHEEL_Y=19;

function bridges(){
 return [
  [1600,2000],[3400,3850],[5500,6000],
  [7600,8000],[10000,10450]
 ];
}

function hill(wx){
 return H*.70
 +Math.sin(wx*.0025)*80
 +Math.sin(wx*.007)*28
 +Math.sin(wx*.0009)*105
 +Math.sin(wx*.014)*7;
}

function ground(wx){
 for(let b of bridges()){
  if(wx>=b[0]&&wx<=b[1]){
   let left=hill(b[0]),right=hill(b[1]);
   return left+(right-left)*(wx-b[0])/(b[1]-b[0]);
  }
 }
 return hill(wx);
}

function createObjects(){
 objects=[];
 for(let i=450;i<100000;i+=520){
  objects.push({x:i,y:ground(i)-68,type:"fuel",used:false});
  for(let j=0;j<4;j++){
   let xx=i+110+j*55;
   objects.push({x:xx,y:ground(xx)-65,type:"coin",used:false});
  }
  let dx=i+320;
  objects.push({x:dx,y:ground(dx)-115,type:"diamond",used:false});
 }
}

function reset(){
 x=100;y=ground(x)-65;
 vx=0;vy=0;angle=0;
 camX=0;camY=0;
 fuel=100;boostPower=100;rpm=0;
 worldDistance=0;
 particles=[];
 createObjects();
}

function start(){
 reset();playing=true;
 document.querySelectorAll(".panel").forEach(e=>e.classList.add("hidden"));
 document.getElementById("hud").classList.remove("hidden");
 document.getElementById("controls").classList.remove("hidden");
}

function home(){
 playing=false;
 document.getElementById("gameOver").classList.add("hidden");
 document.getElementById("hud").classList.add("hidden");
 document.getElementById("controls").classList.add("hidden");
 document.getElementById("menu").classList.remove("hidden");
}

function endGame(){
 playing=false;
 best=Math.max(best,Math.floor(worldDistance));
 document.getElementById("controls").classList.add("hidden");
 document.getElementById("finalDistance").textContent=
 "Distance: "+Math.floor(worldDistance)+" m";
 document.getElementById("gameOver").classList.remove("hidden");
}

function garageOpen(){
 document.getElementById("menu").classList.add("hidden");
 document.getElementById("garage").classList.remove("hidden");
 let html="";
 cars.forEach((c,i)=>{
 let has=owned.includes(i);
 html+=`<div class="vehicle">
 <span>${c.emoji} ${c.name}</span>
 <span>${has?"OWNED":"🪙 "+c.price}</span>
 <button onclick="buyCar(${i})">${selected===i?"SELECTED":has?"SELECT":"BUY"}</button>
 </div>`;
 });
 document.getElementById("carList").innerHTML=html;
}
function buyCar(i){
 if(owned.includes(i))selected=i;
 else if(coins>=cars[i].price){
  coins-=cars[i].price;owned.push(i);selected=i;
 }else{alert("Collect more coins!");return;}
 garageOpen();
}
function garageClose(){
 document.getElementById("garage").classList.add("hidden");
 document.getElementById("menu").classList.remove("hidden");
}

function hold(id,fn){
 let el=document.getElementById(id);
 el.addEventListener("pointerdown",e=>{
  e.preventDefault();fn(true);
 });
 ["pointerup","pointerleave","pointercancel"].forEach(ev=>
 el.addEventListener(ev,()=>fn(false)));
}
hold("gas",v=>gas=v);
hold("brake",v=>brake=v);
hold("boost",v=>boost=v);

addEventListener("keydown",e=>{
 if(["ArrowRight","ArrowLeft","ArrowUp","ArrowDown"," "].includes(e.key))e.preventDefault();
 if(e.key==="ArrowRight"||e.key==="d")gas=true;
 if(e.key==="ArrowLeft"||e.key==="a")brake=true;
 if(e.key===" "||e.key==="ArrowUp")boost=true;
});
addEventListener("keyup",e=>{
 if(e.key==="ArrowRight"||e.key==="d")gas=false;
 if(e.key==="ArrowLeft"||e.key==="a")brake=false;
 if(e.key===" "||e.key==="ArrowUp")boost=false;
});

function addParticles(px,py,color,count){
 for(let i=0;i<count;i++){
  particles.push({
   x:px,y:py,
   vx:(Math.random()-.5)*130,
   vy:-Math.random()*130,
   life:1,color
  });
 }
}

function update(dt){
 if(!playing)return;
 dt=Math.min(dt,.025);
 let car=cars[selected];
 let power=car.power;
 let acceleration=0;

 if(gas)acceleration+=power;
 if(brake)acceleration-=power*.8;
 if(boost&&boostPower>0&&fuel>0){
  acceleration+=power*1.4;
  boostPower=Math.max(0,boostPower-25*dt);
  fuel=Math.max(0,fuel-1.5*dt);
 }else{
  boostPower=Math.min(100,boostPower+9*dt);
 }

 vx+=acceleration/car.mass*dt;
 vx*=Math.pow(.993,dt*60);
 vx=Math.max(-160,Math.min(vx,selected===9?510:410));

 vy+=GRAVITY*dt;
 x+=vx*dt;
 y+=vy*dt;

 // Two-wheel suspension and ground contact
 let cs=Math.cos(angle),sn=Math.sin(angle);
 let contacts=[];
 for(let side of [-1,1]){
  let lx=side*AXLE/2,ly=WHEEL_Y;
  let wx=x+cs*lx-sn*ly;
  let wy=y+sn*lx+cs*ly;
  let gy=ground(wx);
  contacts.push({wx,wy,gy,penetration:wy+car.r-gy});
 }

 let touching=contacts.filter(c=>c.penetration>0);
 grounded=touching.length>0;

 if(grounded){
  let correction=0;
  for(let c of touching)correction+=c.penetration;
  correction/=touching.length;
  y-=correction*.82;
  if(vy>0)vy=0;
  // Follow the slope without uncontrolled spinning
  let rear=ground(x-AXLE/2);
  let front=ground(x+AXLE/2);
  let target=Math.atan2(front-rear,AXLE);
  let difference=Math.atan2(Math.sin(target-angle),Math.cos(target-angle));
  angle+=difference*Math.min(1,dt*8);
 }else{
  // Air control is gentle
  if(gas)angle+=dt*.8;
  if(brake)angle-=dt*.8;
 }

 angle=Math.max(-1.15,Math.min(1.15,angle));

 // Stop extreme falls and reset only when genuinely out of the map
 if(y>H+550||x<0){endGame();return;}

 // Pickups
 for(let o of objects){
  if(o.used)continue;
  if(Math.abs(o.x-x)<42&&Math.abs(o.y-(y+20))<100){
   o.used=true;
   if(o.type==="fuel"){
    fuel=Math.min(100,fuel+35);
    addParticles(o.x,o.y,"#ff5252",12);
   }
   if(o.type==="coin"){
    coins++;addParticles(o.x,o.y,"#ffdb36",5);
   }
   if(o.type==="diamond"){
    diamonds++;coins+=5;
    addParticles(o.x,o.y,"#51eaff",18);
   }
  }
 }

 fuel=Math.max(0,fuel-dt*(gas?1.15:.18));
 if(fuel<=0){endGame();return;}

 rpm=Math.min(9000,Math.max(0,Math.abs(vx)*17+(gas?1000:0)));
 worldDistance=Math.max(0,(x-100)/10);

 for(let p of particles){
  p.x+=p.vx*dt;p.y+=p.vy*dt;
  p.vy+=250*dt;p.life-=dt;
 }
 particles=particles.filter(p=>p.life>0);

 camX=Math.max(0,x-W*.34);
 camY=y-H*.43;
}

function drawBridge(b){
 let left=b[0]-camX,right=b[1]-camX;
 if(right<0||left>W)return;
 let yl=ground(b[0])-camY,yr=ground(b[1])-camY;
 ctx.strokeStyle="#5b3822";ctx.lineWidth=13;
 ctx.beginPath();ctx.moveTo(left,yl);ctx.lineTo(right,yr);ctx.stroke();
 ctx.strokeStyle="#d0a05e";ctx.lineWidth=8;
 ctx.beginPath();ctx.moveTo(left,yl-4);ctx.lineTo(right,yr-4);ctx.stroke();
 for(let xx=b[0];xx<=b[1];xx+=24){
  let yy=ground(xx)-camY;
  ctx.strokeStyle="#68472b";ctx.lineWidth=3;
  ctx.beginPath();ctx.moveTo(xx-camX,yy-8);
  ctx.lineTo(xx-camX,yy+5);ctx.stroke();
 }
 // Railings
 ctx.strokeStyle="#704526";ctx.lineWidth=4;
 ctx.beginPath();
 ctx.moveTo(left,yl-22);ctx.lineTo(right,yr-22);ctx.stroke();
 for(let xx=b[0];xx<=b[1];xx+=65){
  let yy=ground(xx)-camY;
  ctx.beginPath();ctx.moveTo(xx-camX,yy-23);
  ctx.lineTo(xx-camX,yy-5);ctx.stroke();
 }
}

function drawCar(){
 let car=cars[selected];
 let sx=x-camX,sy=y-camY;
 ctx.save();
 ctx.translate(sx,sy);
 ctx.rotate(angle);

 // Suspension
 ctx.strokeStyle="#30363a";ctx.lineWidth=5;
 ctx.beginPath();
 ctx.moveTo(-AXLE/2,WHEEL_Y);
 ctx.lineTo(-AXLE/2+7,WHEEL_Y+12);
 ctx.moveTo(AXLE/2,WHEEL_Y);
 ctx.lineTo(AXLE/2-7,WHEEL_Y+12);
 ctx.stroke();

 // Vehicle body
 const colors=["#bbb","#eee","#e44","#268be0","#c69b35",
 "#dd7834","#edc62d","#32c7dc","#42b66b","#df3434"];
 let col=colors[selected];
 ctx.fillStyle=col;
 if(selected===0||selected===1){
  ctx.fillRect(-34,-7,68,8);
  ctx.strokeStyle="#eee";ctx.lineWidth=3;
  ctx.beginPath();ctx.moveTo(-25,0);ctx.lineTo(-8,-25);
  ctx.lineTo(24,0);ctx.stroke();
 }else{
  ctx.fillRect(-48,-20,96,27);
  ctx.fillRect(-27,-43,48,25);
  ctx.fillStyle="#9ee8ff";ctx.fillRect(-21,-39,35,15);
  if(selected===4)ctx.fillRect(-53,-10,18,13);
  if(selected===5||selected===9)ctx.fillRect(32,-34,29,30);
  if(selected===6){
   ctx.strokeStyle="#d8aa24";ctx.lineWidth=7;
   ctx.beginPath();ctx.moveTo(37,-10);ctx.lineTo(67,-56);
   ctx.lineTo(85,-21);ctx.stroke();
  }
  if(selected===7)ctx.fillRect(-53,3,106,10);
 }
 // Driver
 ctx.fillStyle="#ffd1a2";
 ctx.beginPath();ctx.arc(-3,-49,10,0,Math.PI*2);ctx.fill();
 ctx.fillStyle="#2354a0";
 ctx.fillRect(-14,-40,24,20);
 ctx.strokeStyle="#f4c6a0";ctx.lineWidth=5;
 ctx.beginPath();ctx.moveTo(-10,-30);ctx.lineTo(-23,-19);
 ctx.moveTo(8,-30);ctx.lineTo(20,-19);ctx.stroke();

 // Wheels
 for(let side of [-1,1]){
  ctx.fillStyle="#222";
  ctx.beginPath();ctx.arc(side*AXLE/2,WHEEL_Y,car.r,0,Math.PI*2);ctx.fill();
  ctx.strokeStyle="#aaa";ctx.lineWidth=4;
  ctx.beginPath();ctx.arc(side*AXLE/2,WHEEL_Y,car.r*.55,0,Math.PI*2);ctx.stroke();
  ctx.fillStyle="#ddd";
  ctx.beginPath();ctx.arc(side*AXLE/2,WHEEL_Y,4,0,Math.PI*2);ctx.fill();
 }
 ctx.restore();
}

function draw(){
 ctx.clearRect(0,0,W,H);
 let sky=ctx.createLinearGradient(0,0,0,H);
 sky.addColorStop(0,"#55b9ee");
 sky.addColorStop(1,"#e3f6ff");
 ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);

 // Sun and distant mountains
 ctx.fillStyle="#fff1a0";
 ctx.beginPath();ctx.arc(W*.82,H*.17,35,0,7);ctx.fill();
 ctx.fillStyle="#91b8a4";
 ctx.beginPath();ctx.moveTo(0,H);
 for(let sx=0;sx<=W;sx+=8)
  ctx.lineTo(sx,H*.57+Math.sin((sx+camX*.15)*.004)*48);
 ctx.lineTo(W,H);ctx.fill();

 // Main hill ground
 ctx.fillStyle="#668b43";
 ctx.beginPath();ctx.moveTo(0,H);
 for(let sx=0;sx<=W;sx+=5)
  ctx.lineTo(sx,ground(sx+camX)-camY);
 ctx.lineTo(W,H);ctx.fill();

 ctx.strokeStyle="#3d662c";ctx.lineWidth=5;
 ctx.beginPath();
 for(let sx=0;sx<=W;sx+=5){
  let sy=ground(sx+camX)-camY;
  if(sx===0)ctx.moveTo(sx,sy);else ctx.lineTo(sx,sy);
 }
 ctx.stroke();

 // Bridges over the valleys
 bridges().forEach(drawBridge);

 // Pickups
 for(let o of objects){
  if(o.used)continue;
  let sx=o.x-camX,sy=o.y-camY;
  if(sx<-40||sx>W+40)continue;
  if(o.type==="fuel"){
   ctx.fillStyle="#ed2929";ctx.fillRect(sx-11,sy-15,22,30);
   ctx.fillStyle="white";ctx.fillRect(sx-4,sy-10,8,19);
   ctx.fillRect(sx-8,sy-5,16,7);
  }
  if(o.type==="coin"){
   ctx.fillStyle="#ffcf22";
   ctx.beginPath();ctx.arc(sx,sy,10,0,7);ctx.fill();
   ctx.fillStyle="#a36b00";ctx.font="bold 12px Arial";
   ctx.fillText("$",sx-4,sy+4);
  }
  if(o.type==="diamond"){
   ctx.fillStyle="#49eaff";
   ctx.beginPath();ctx.moveTo(sx,sy-15);
   ctx.lineTo(sx+12,sy);ctx.lineTo(sx,sy+17);
   ctx.lineTo(sx-12,sy);ctx.closePath();ctx.fill();
   ctx.strokeStyle="white";ctx.lineWidth=2;ctx.stroke();
  }
 }

 drawCar();

 for(let p of particles){
  ctx.globalAlpha=Math.max(0,p.life);
  ctx.fillStyle=p.color;
  ctx.beginPath();ctx.arc(p.x-camX,p.y-camY,4,0,7);ctx.fill();
 }
 ctx.globalAlpha=1;

 if(playing){
  document.getElementById("coins").textContent=coins;
  document.getElementById("diamonds").textContent=diamonds;
  document.getElementById("fuel").textContent=Math.ceil(fuel);
  document.getElementById("distance").textContent=Math.floor(worldDistance);
  document.getElementById("rpm").textContent=Math.floor(rpm);
  document.getElementById("rpmFill").style.width=(rpm/9000*100)+"%";
  document.getElementById("boostText").textContent=Math.floor(boostPower);
  document.getElementById("boostFill").style.width=boostPower+"%";
 }
}

let previous=0;
function loop(time){
 let dt=(time-previous)/1000;
 previous=time;
 update(dt||0);
 draw();
 requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>

Game Source: Hill Climb Adventure 2

Creator: BlazeGalaxy19

Libraries: none

Complexity: complex (534 lines, 15.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: hill-climb-adventure-2-blazegalaxy19" to link back to the original. Then publish at arcadelab.ai/publish.