🎮ArcadeLab

Hill Climb Adventure

by BlazeGalaxy19
484 lines12.0 KB
▶ Play

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Hill Climb Adventure</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#172b3d;color:white;font-family:Arial;text-align:center}
#hud{display:flex;justify-content:space-around;gap:8px;padding:12px;background:#102030;flex-wrap:wrap}
canvas{display:block;width:100%;height:62vh;background:#87ceeb}
button{border:0;border-radius:10px;padding:12px;font-weight:bold;cursor:pointer}
#controls{display:flex;justify-content:space-around;padding:12px;background:#172b3d}
.pedal{width:42%;height:65px;font-size:21px;color:white;touch-action:none}
#gas{background:#159447}#brake{background:#bd3838}
#menu{padding:10px}
#shop{display:none;padding:12px;background:#203b54}
#shop button{margin:5px}
#message{min-height:25px;color:#ffdf69;font-weight:bold;padding:5px}
</style>
</head>
<body>

<div id="hud">
 <span>🪙 <b id="coins">0</b></span>
 <span>⛽ <b id="fuel">100</b>%</span>
 <span>📏 <b id="distance">0</b> m</span>
 <span>🏆 <b id="best">0</b> m</span>
</div>

<canvas id="game"></canvas>
<div id="message">HILL CLIMB ADVENTURE</div>

<div id="menu">
 <button onclick="startGame()">▶ PLAY</button>
 <button onclick="showVehicles()">🚗 VEHICLES</button>
 <button onclick="showMaps()">🌎 MAPS</button>
</div>

<div id="shop">
 <h3 id="shopTitle">SHOP</h3>
 <div id="shopItems"></div>
 <button onclick="closeShop()">CLOSE</button>
</div>

<div id="controls">
 <button class="pedal" id="brake">◀ BRAKE</button>
 <button class="pedal" id="gas">GAS ▶</button>
</div>

<script>
const canvas=document.getElementById("game");
const ctx=canvas.getContext("2d");

function resize(){
 canvas.width=canvas.clientWidth;
 canvas.height=canvas.clientHeight;
}
resize();
window.addEventListener("resize",resize);

const vehicles=[
 {name:"Mini Car",price:0,color:"#ffd52e",power:1},
 {name:"Bike",price:500,color:"#ff4545",power:1.15},
 {name:"Truck",price:1200,color:"#e58b32",power:.9},
 {name:"Tractor",price:2000,color:"#38bd4c",power:.85},
 {name:"JCB",price:3000,color:"#ffb52b",power:.8},
 {name:"Cycle",price:4000,color:"#45c8ff",power:.75},
 {name:"Skateboard",price:5000,color:"#d75aff",power:1.3},
 {name:"Jet Ski",price:6500,color:"#30e5d4",power:1.2},
 {name:"Monster Truck",price:8000,color:"#a7d83b",power:1.1},
 {name:"Super Car",price:10000,color:"#eeeeee",power:1.5}
];

const maps=[
 {name:"Green Hills",price:0,sky:"#87ceeb",ground:"#479b42"},
 {name:"Desert",price:1500,sky:"#f4c477",ground:"#c78a43"},
 {name:"Snow Land",price:3000,sky:"#bce8ff",ground:"#e9f5fa"},
 {name:"Forest",price:5000,sky:"#667d79",ground:"#315a3c"},
 {name:"Moon",price:8000,sky:"#20233f",ground:"#777b8a"}
];

let coins=+(localStorage.hcCoins||0);
let best=+(localStorage.hcBest||0);
let owned=JSON.parse(localStorage.hcOwned||"[0]");
let unlocked=JSON.parse(localStorage.hcMaps||"[0]");
let vehicle=0,map=0;

let playing=false;
let fuel=100,distance=0,speed=0;
let camera=0,carY=0,vy=0,angle=0;
let grounded=true,boost=100;
let gas=false,brake=false;
let keys={};
let lastTime=0;
let messageTime=0;
let bridgeUsed=new Set();

const $=id=>document.getElementById(id);

function save(){
 localStorage.hcCoins=coins;
 localStorage.hcBest=best;
 localStorage.hcOwned=JSON.stringify(owned);
 localStorage.hcMaps=JSON.stringify(unlocked);
 updateHUD();
}
function updateHUD(){
 $("coins").textContent=coins;
 $("fuel").textContent=Math.floor(fuel);
 $("distance").textContent=Math.floor(distance);
 $("best").textContent=best;
}
function say(text){
 $("message").textContent=text;
 messageTime=2.5;
}

function terrain(x){
 const difficulty=Math.min(x/9000,2);
 return canvas.height*.70
 +Math.sin(x*.0035)*35
 +Math.sin(x*.008)*15
 +Math.sin(x*.0015)*45
 +Math.sin(x*.014)*8
 -difficulty*12*Math.sin(x*.002);
}

function startGame(){
 closeShop();
 playing=true;
 fuel=100;
 distance=0;
 speed=0;
 camera=0;
 vy=0;
 angle=0;
 boost=100;
 bridgeUsed.clear();
 carY=terrain(0)-35;
 grounded=true;
 lastTime=performance.now();
 say("GO! DRIVE AS FAR AS YOU CAN!");
 requestAnimationFrame(loop);
}

function endGame(reason){
 playing=false;
 if(distance>best)best=Math.floor(distance);
 save();
 say(reason+" — DISTANCE: "+Math.floor(distance)+" m");
 $("menu").style.display="block";
}

function showVehicles(){
 playing=false;
 $("menu").style.display="none";
 $("controls").style.display="none";
 $("shop").style.display="block";
 $("shopTitle").textContent="🚗 VEHICLE SHOP";
 let html="";
 vehicles.forEach((v,i)=>{
 html+=`<button onclick="selectVehicle(${i})">
 ${v.name}<br>${owned.includes(i)?"SELECT":"BUY "+v.price+" 🪙"}
 </button>`;
 });
 $("shopItems").innerHTML=html;
}
function selectVehicle(i){
 if(!owned.includes(i)){
  if(coins<vehicles[i].price){say("NOT ENOUGH COINS!");return}
  coins-=vehicles[i].price;
  owned.push(i);
 }
 vehicle=i;
 save();
 showVehicles();
 say(vehicles[i].name+" SELECTED!");
}
function showMaps(){
 playing=false;
 $("menu").style.display="none";
 $("controls").style.display="none";
 $("shop").style.display="block";
 $("shopTitle").textContent="🌎 MAP SHOP";
 let html="";
 maps.forEach((m,i)=>{
 html+=`<button onclick="selectMap(${i})">
 ${m.name}<br>${unlocked.includes(i)?"SELECT":"BUY "+m.price+" 🪙"}
 </button>`;
 });
 $("shopItems").innerHTML=html;
}
function selectMap(i){
 if(!unlocked.includes(i)){
  if(coins<maps[i].price){say("NOT ENOUGH COINS!");return}
  coins-=maps[i].price;
  unlocked.push(i);
 }
 map=i;
 save();
 closeShop();
 say(maps[i].name+" SELECTED!");
}
function closeShop(){
 $("shop").style.display="none";
 $("menu").style.display="block";
 $("controls").style.display="flex";
}

function drawBackground(){
 ctx.fillStyle=maps[map].sky;
 ctx.fillRect(0,0,canvas.width,canvas.height);
 ctx.fillStyle="rgba(50,90,90,.20)";
 ctx.beginPath();
 ctx.moveTo(0,canvas.height);
 for(let x=0;x<=canvas.width+20;x+=20){
  ctx.lineTo(x,canvas.height*.52+Math.sin((x+camera*.2)*.005)*35);
 }
 ctx.lineTo(canvas.width,canvas.height);
 ctx.fill();
}

function drawGround(){
 ctx.fillStyle=maps[map].ground;
 ctx.beginPath();
 ctx.moveTo(0,canvas.height);
 for(let x=0;x<=canvas.width+10;x+=6){
  ctx.lineTo(x,terrain(camera+x));
 }
 ctx.lineTo(canvas.width,canvas.height);
 ctx.fill();
 ctx.strokeStyle="#294a2c";
 ctx.lineWidth=4;
 ctx.stroke();
}

function drawBridge(worldX){
 const sx=worldX-camera;
 if(sx<-160||sx>canvas.width+160)return;
 const y=terrain(worldX)-10;
 ctx.fillStyle="#77543a";
 ctx.fillRect(sx,y,150,17);
 ctx.fillStyle="#c69a60";
 for(let i=0;i<7;i++)ctx.fillRect(sx+i*22,y-6,12,6);
}

function drawFuel(worldX){
 const sx=worldX-camera;
 if(sx<-40||sx>canvas.width+40)return;
 const y=terrain(worldX)-48;
 ctx.fillStyle="white";
 ctx.fillRect(sx-12,y-20,24,27);
 ctx.fillStyle="#e53232";
 ctx.fillRect(sx-8,y-15,16,18);
 ctx.fillStyle="white";
 ctx.font="bold 13px Arial";
 ctx.fillText("F",sx-4,y-2);
}

function drawCoin(worldX){
 const sx=worldX-camera;
 if(sx<-30||sx>canvas.width+30)return;
 const y=terrain(worldX)-65;
 ctx.fillStyle="#ffdf25";
 ctx.beginPath();
 ctx.arc(sx,y,10,0,Math.PI*2);
 ctx.fill();
 ctx.strokeStyle="#a87900";
 ctx.lineWidth=2;
 ctx.stroke();
}

function drawCar(){
 const v=vehicles[vehicle];
 const x=canvas.width*.28;
 ctx.save();
 ctx.translate(x,carY);
 ctx.rotate(angle);

 ctx.fillStyle="#202020";
 ctx.beginPath();ctx.arc(-25,18,13,0,Math.PI*2);ctx.fill();
 ctx.beginPath();ctx.arc(25,18,13,0,Math.PI*2);ctx.fill();

 ctx.fillStyle="#aaa";
 ctx.beginPath();ctx.arc(-25,18,5,0,Math.PI*2);ctx.fill();
 ctx.beginPath();ctx.arc(25,18,5,0,Math.PI*2);ctx.fill();

 ctx.fillStyle=v.color;
 ctx.fillRect(-43,-10,86,25);
 ctx.fillStyle="#b9eaff";
 ctx.fillRect(-18,-25,30,17);

 ctx.fillStyle="#222";
 ctx.beginPath();ctx.arc(-3,-29,8,0,Math.PI*2);ctx.fill();
 ctx.restore();
}

function drawMeters(){
 ctx.fillStyle="#172b3b";
 ctx.fillRect(12,12,145,14);
 ctx.fillStyle="#e83b3b";
 ctx.fillRect(12,12,145*fuel/100,14);
 ctx.strokeStyle="white";
 ctx.strokeRect(12,12,145,14);
 ctx.fillStyle="white";
 ctx.font="12px Arial";
 ctx.fillText("FUEL",12,40);

 ctx.fillStyle="#172b3b";
 ctx.fillRect(12,48,145,10);
 ctx.fillStyle="#30d7ed";
 ctx.fillRect(12,48,145*boost/100,10);
 ctx.strokeStyle="white";
 ctx.strokeRect(12,48,145,10);
 ctx.fillStyle="white";
 ctx.fillText("BOOST (SPACE)",12,73);
}

function draw(){
 drawBackground();
 drawGround();

 // Draw upcoming bridges
 for(let i=Math.floor(camera/1800);i<=Math.floor(camera/1800)+2;i++){
  drawBridge(i*1800+900);
 }

 // Fuel cans: 2250 world units apart.
 for(let i=Math.floor(camera/2250);i<=Math.floor(camera/2250)+2;i++){
  if(i>=0)drawFuel((i+1)*2250);
 }

 // Rare coins: one small group every 1200 units.
 for(let i=Math.floor(camera/1200);i<=Math.floor(camera/1200)+2;i++){
  if(i>=0&&i%2===0)drawCoin((i+1)*1200);
 }

 drawCar();
 drawMeters();

 ctx.fillStyle="white";
 ctx.font="bold 15px Arial";
 ctx.fillText(maps[map].name,canvas.width-130,25);
}

function loop(now){
 if(!playing)return;
 const dt=Math.min((now-lastTime)/1000,.04);
 lastTime=now;

 const v=vehicles[vehicle];
 const right=gas||keys.ArrowRight||keys.d;
 const left=brake||keys.ArrowLeft||keys.a;
 const boosting=keys[" "]&&boost>0&&fuel>0;

 // Fuel drains at 2 percent every second.
 fuel=Math.max(0,fuel-2*dt);

 // Accelerator and brake/reverse.
 if(right&&fuel>0)speed+=95*v.power*dt;
 if(left)speed-=150*dt;

 if(!right&&!left)speed*=Math.pow(.985,dt*60);
 if(boosting)speed+=170*dt;

 speed=Math.max(-45,Math.min(speed,125*v.power));
 if(boosting)boost=Math.max(0,boost-30*dt);
 else boost=Math.min(100,boost+7*dt);

 // Forward distance and camera.
 const dx=speed*dt;
 distance=Math.max(0,distance+Math.max(0,dx));
 camera=Math.max(0,camera+dx);

 const worldX=camera+canvas.width*.28;
 const groundY=terrain(worldX)-35;

 // Gravity and wheel contact.
 vy+=650*dt;
 carY+=vy*dt;
 grounded=false;

 if(carY>=groundY){
  carY=groundY;
  vy=0;
  grounded=true;
  const slope=(terrain(worldX+10)-terrain(worldX-10))/20;
  const target=Math.atan(slope);
  angle+=(target-angle)*Math.min(1,dt*7);
 }else{
  if(right)angle-=1.4*dt;
  if(left)angle+=1.4*dt;
 }

 // Bridge launches the car upward once per bridge.
 const bridgeIndex=Math.floor(worldX/1800);
 const bridgeStart=bridgeIndex*1800+900;
 if(worldX>=bridgeStart&&worldX<=bridgeStart+150&&!bridgeUsed.has(bridgeIndex)){
  vy=-390;
  grounded=false;
  bridgeUsed.add(bridgeIndex);
  say("BRIDGE JUMP!");
 }

 // Collect fuel only when close to the actual can.
 const fuelIndex=Math.round(worldX/2250);
 const fuelX=fuelIndex*2250;
 if(fuelIndex>0&&Math.abs(worldX-fuelX)<45&&
    Math.abs(carY-(terrain(fuelX)-48))<100){
  if(!window.collectedFuel)window.collectedFuel=new Set();
  if(!window.collectedFuel.has(fuelIndex)){
   window.collectedFuel.add(fuelIndex);
   fuel=Math.min(100,fuel+45);
   say("FUEL +45%!");
  }
 }

 // Collect rare coins.
 const coinIndex=Math.round(worldX/1200);
 const coinX=coinIndex*1200;
 if(coinIndex>0&&coinIndex%2===0&&Math.abs(worldX-coinX)<40&&
    Math.abs(carY-(terrain(coinX)-65))<110){
  if(!window.collectedCoins)window.collectedCoins=new Set();
  if(!window.collectedCoins.has(coinIndex)){
   window.collectedCoins.add(coinIndex);
   coins+=25;
   save();
   say("+25 COINS!");
  }
 }

 // Crash if upside down while on the ground.
 if(grounded&&Math.abs(angle)>2.5){
  endGame("CRASH!");
  return;
 }

 if(fuel<=0&&speed<5){
  endGame("OUT OF FUEL!");
  return;
 }

 if(distance>best)best=Math.floor(distance);
 if(messageTime>0)messageTime-=dt;
 else $("message").textContent="Keep driving! Beat your best distance!";

 updateHUD();
 draw();
 requestAnimationFrame(loop);
}

// Keyboard controls
window.addEventListener("keydown",e=>{
 if(["ArrowRight","ArrowLeft"," "].includes(e.key))e.preventDefault();
 keys[e.key===" "?" ":e.key]=true;
});
window.addEventListener("keyup",e=>{
 keys[e.key===" "?" ":e.key]=false;
});

// Touch controls
function hold(id,action){
 const el=$(id);
 el.addEventListener("pointerdown",e=>{
  e.preventDefault();action(true);
 });
 ["pointerup","pointerleave","pointercancel"].forEach(event=>{
  el.addEventListener(event,e=>{
   e.preventDefault();action(false);
  });
 });
}
hold("gas",v=>gas=v);
hold("brake",v=>brake=v);

// Initial screen
draw();
updateHUD();
</script>
</body>
</html>

Game Source: Hill Climb Adventure

Creator: BlazeGalaxy19

Libraries: none

Complexity: complex (484 lines, 12.0 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-blazegalaxy19-mwaumpze" to link back to the original. Then publish at arcadelab.ai/publish.