🎮ArcadeLab

Hill Climb Legends

by BlazeGalaxy19
454 lines15.9 KB
▶ Play

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Hill Climb Legends</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#071321;color:white;font-family:Arial;display:flex;justify-content:center;align-items:center;min-height:100vh}
#wrap{width:min(1000px,100vw)}
canvas{width:100%;display:block;border:2px solid #46dfff;border-radius:12px;touch-action:none}
.controls{display:flex;justify-content:space-between;padding:9px;gap:12px}
button{border:2px solid #5ce4ff;border-radius:12px;background:#124663;color:white;padding:14px 20px;font-size:17px;font-weight:bold;cursor:pointer;touch-action:none}
#gas{background:#17623b;border-color:#72ff9d}
#brake{background:#733a22;border-color:#ffbf69}
#hint{text-align:center;font-size:13px;color:#b8cce0;padding:4px}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game" width="1000" height="600"></canvas>
<div class="controls">
<button id="brake">◀ BRAKE</button>
<button id="gas">GAS ▶</button>
</div>
<div id="hint">A / LEFT = Brake | D / RIGHT = Gas | ESC = Pause</div>
</div>
<script>
const c=document.getElementById("game"),ctx=c.getContext("2d");
const W=1000,H=600;

const VEHICLES=[
 ["Mini Buggy",0,"#e65b38",1],
 ["Blue Jeep",80,"#398de0",1.15],
 ["Monster Truck",160,"#a64be0",1.3],
 ["Racing Car",240,"#e6bd38",1.2],
 ["Green 4x4",350,"#42b96a",1.4],
 ["Desert Rover",480,"#d58b48",1.5],
 ["Snow Van",650,"#b9e6ee",1.6],
 ["Super Bike",850,"#ed557c",1.7],
 ["Armored Truck",1100,"#7b8d9d",1.8],
 ["Legend Beast",1500,"#ff6b28",2]
];

const MAPS=[
 ["Green Hills",0,"#65b84b","#8bd7ff"],
 ["Desert Road",250,"#d5a04c","#f4c77b"],
 ["Snow Mountains",500,"#d9edf5","#9bd7f3"],
 ["Night Valley",800,"#283c75","#111b3e"]
];

let state="menu",difficulty="normal",vehicle=0,map=0;
let ownedVehicles=JSON.parse(localStorage.getItem("hlVehicles")||"[0]");
let ownedMaps=JSON.parse(localStorage.getItem("hlMaps")||"[0]");
let coins=Number(localStorage.getItem("hlCoins")||0);
let best=Number(localStorage.getItem("hlBest")||0);
let score=0,distance=0,fuel=100,runCoins=0;
let car={x:180,y:300,vx:0,vy:0,angle:0};
let camera=0,gold=[],fuelItems=[],particles=[];
let gas=false,brake=false,last=0;
let wheelSpin=0,suspension=0;
let audio=null,musicTimer=null,musicIndex=0;
let soundOn=true,musicOn=true;

function save(){
 localStorage.setItem("hlVehicles",JSON.stringify(ownedVehicles));
 localStorage.setItem("hlMaps",JSON.stringify(ownedMaps));
 localStorage.setItem("hlCoins",coins);
 localStorage.setItem("hlBest",best);
}
function audioStart(){
 try{
  if(!audio)audio=new(window.AudioContext||window.webkitAudioContext)();
  if(audio.state==="suspended")audio.resume();
 }catch(e){}
}
function tone(freq,duration=.15,type="sine",volume=.035){
 if(!soundOn)return;
 audioStart();
 if(!audio)return;
 let o=audio.createOscillator(),g=audio.createGain();
 o.type=type;o.frequency.value=freq;
 g.gain.setValueAtTime(volume,audio.currentTime);
 g.gain.exponentialRampToValueAtTime(.001,audio.currentTime+duration);
 o.connect(g);g.connect(audio.destination);
 o.start();o.stop(audio.currentTime+duration);
}
function musicStop(){
 if(musicTimer){clearInterval(musicTimer);musicTimer=null}
}
function musicStart(){
 if(!musicOn)return;
 audioStart();musicStop();
 let notes=[110,165,220,165,130,196,247,196,110,165,220,247];
 musicTimer=setInterval(()=>{
  if(state!=="play"||!musicOn)return;
  tone(notes[musicIndex%notes.length],.25,"triangle",.012);
  if(musicIndex%3===0)tone(55,.2,"sine",.014);
  musicIndex++;
 },260);
}
function gameSound(){
 tone(170,.3,"sawtooth",.05);
 setTimeout(()=>tone(100,.5,"triangle",.05),180);
 setTimeout(()=>tone(65,.7,"sawtooth",.04),350);
}
function terrain(x){
 let base=405,rough=difficulty==="hard"?1.45:1;
 if(map===0)return base+(Math.sin(x*.006)*65+Math.sin(x*.014)*22+Math.sin(x*.002)*38)*rough;
 if(map===1)return base+(Math.sin(x*.004)*45+Math.sin(x*.013)*18)*rough;
 if(map===2)return base+(Math.sin(x*.009)*90+Math.sin(x*.017)*35)*rough;
 return base+(Math.sin(x*.007)*75+Math.sin(x*.019)*25)*rough;
}
function createItems(){
 gold=[];fuelItems=[];
 for(let i=0;i<150;i++){
  let x=350+i*190+Math.random()*60;
  gold.push({x,y:terrain(x)-75-Math.random()*30,taken:false});
  if(i%5===2)fuelItems.push({x:x+75,y:terrain(x+75)-70,taken:false});
 }
}
function startGame(){
 audioStart();
 score=0;distance=0;runCoins=0;fuel=100;camera=0;
 car={x:180,y:terrain(180)-40,vx:0,vy:0,angle:0};
 wheelSpin=0;suspension=0;
 createItems();state="play";musicStart();
}
function finish(){
 if(state!=="play")return;
 state="over";musicStop();gameSound();
 best=Math.max(best,Math.floor(distance));save();
}
function roundedRect(x,y,w,h,r,color){
 ctx.fillStyle=color;ctx.beginPath();ctx.roundRect(x,y,w,h,r);ctx.fill();
}
function background(){
 let sky=ctx.createLinearGradient(0,0,0,H);
 sky.addColorStop(0,MAPS[map][3]);
 sky.addColorStop(1,MAPS[map][2]);
 ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);
 if(map===3){
  for(let i=0;i<45;i++){
   ctx.fillStyle="rgba(255,255,255,.65)";
   ctx.fillRect((i*173)%W,(i*97)%300,2,2);
  }
  ctx.fillStyle="#fff0a5";ctx.beginPath();ctx.arc(800,105,40,0,7);ctx.fill();
 }else{
  ctx.fillStyle=map===2?"#ffffff":"#fff0a5";
  ctx.beginPath();ctx.arc(800,105,45,0,7);ctx.fill();
 }
 for(let i=0;i<9;i++){
  let x=(i*155-camera*.18%155);
  ctx.fillStyle=map===2?"#a9c9d7":map===1?"#bd8b4e":"#739e8e";
  ctx.beginPath();ctx.moveTo(x,430);
  ctx.lineTo(x+90,210+(i%3)*40);
  ctx.lineTo(x+220,430);ctx.fill();
 }
}
function drawGround(){
 ctx.beginPath();ctx.moveTo(0,H);
 for(let x=0;x<=W;x+=5)ctx.lineTo(x,terrain(x+camera));
 ctx.lineTo(W,H);ctx.closePath();
 let g=ctx.createLinearGradient(0,350,0,H);
 g.addColorStop(0,MAPS[map][2]);
 g.addColorStop(1,map===2?"#7898a8":map===1?"#8c5c32":"#235236");
 ctx.fillStyle=g;ctx.fill();
 ctx.strokeStyle=map===2?"#fff":"#c6f08a";ctx.lineWidth=6;ctx.stroke();
}
function drawItems(){
 gold.forEach(o=>{
  if(o.taken)return;
  let x=o.x-camera;
  if(x< -30||x>W+30)return;
  ctx.fillStyle="#ffd43b";ctx.beginPath();ctx.arc(x,o.y,13,0,7);ctx.fill();
  ctx.strokeStyle="#fff5ae";ctx.lineWidth=3;ctx.stroke();
  ctx.fillStyle="#8c5c00";ctx.font="bold 14px Arial";ctx.fillText("$",x-4,o.y+5);
 });
 fuelItems.forEach(o=>{
  if(o.taken)return;
  let x=o.x-camera;
  if(x< -30||x>W+30)return;
  roundedRect(x-12,o.y-15,24,29,5,"#ec3f3f");
  ctx.fillStyle="white";ctx.font="bold 14px Arial";ctx.fillText("F",x-5,o.y+5);
 });
}
function drawCar(){
 let x=car.x-camera,y=car.y+suspension;
 let v=VEHICLES[vehicle],type=vehicle;
 ctx.save();ctx.translate(x,y);ctx.rotate(car.angle);

 // Animated wheels
 for(let wx of [-38,38]){
  ctx.save();ctx.translate(wx,18);ctx.rotate(wheelSpin);
  ctx.fillStyle="#141b25";ctx.beginPath();ctx.arc(0,0,20,0,Math.PI*2);ctx.fill();
  ctx.strokeStyle="#aab8c5";ctx.lineWidth=4;ctx.stroke();
  ctx.strokeStyle="#e2eaf0";ctx.lineWidth=3;
  for(let i=0;i<4;i++){
   ctx.rotate(Math.PI/2);ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(0,-15);ctx.stroke();
  }
  ctx.fillStyle="#71808c";ctx.beginPath();ctx.arc(0,0,7,0,Math.PI*2);ctx.fill();
  ctx.restore();
 }

 // Different body shapes
 ctx.fillStyle=v[2];
 if(type===2||type===8){
  ctx.beginPath();ctx.moveTo(-60,-10);ctx.lineTo(-48,-38);
  ctx.lineTo(40,-38);ctx.lineTo(60,-10);ctx.closePath();ctx.fill();
 }else if(type===7){
  ctx.fillRect(-32,-12,65,12);
  ctx.beginPath();ctx.moveTo(-15,-12);ctx.lineTo(8,-37);ctx.lineTo(35,-12);ctx.fill();
 }else if(type===3){
  ctx.beginPath();ctx.moveTo(-55,-10);ctx.lineTo(-35,-27);
  ctx.lineTo(25,-27);ctx.lineTo(55,-10);ctx.closePath();ctx.fill();
 }else{
  ctx.beginPath();ctx.roundRect(-55,-17,110,35,8);ctx.fill();
  ctx.fillRect(-32,-42,65,27);
 }
 // Windows
 ctx.fillStyle="#9deaff";ctx.fillRect(-12,-36,28,19);

 // Dummy driver
 ctx.fillStyle="#f3bd91";ctx.beginPath();ctx.arc(-20,-31,11,0,Math.PI*2);ctx.fill();
 ctx.fillStyle="#25374d";ctx.fillRect(-30,-20,22,19);
 ctx.strokeStyle="#29374d";ctx.lineWidth=4;
 ctx.beginPath();ctx.moveTo(-10,-12);ctx.lineTo(1,-4);ctx.stroke();

 // Headlight
 ctx.fillStyle="#fff1a3";ctx.fillRect(48,-8,10,8);

 // Suspension
 ctx.strokeStyle="#d7e2ed";ctx.lineWidth=4;
 ctx.beginPath();ctx.moveTo(-38,0);ctx.lineTo(-38,18);
 ctx.moveTo(38,0);ctx.lineTo(38,18);ctx.stroke();
 ctx.restore();
}
function drawHUD(){
 roundedRect(15,15,345,115,12,"rgba(5,18,35,.82)");
 ctx.fillStyle="white";ctx.font="bold 20px Arial";
 ctx.fillText("DISTANCE: "+Math.floor(distance)+" m",30,45);
 ctx.fillText("COINS: "+coins+" (+"+runCoins+")",30,75);
 ctx.fillText("FUEL",30,107);
 ctx.fillStyle="#263445";ctx.fillRect(100,91,235,18);
 ctx.fillStyle=fuel>25?"#4df08c":"#ff5050";
 ctx.fillRect(100,91,235*fuel/100,18);
 ctx.fillStyle="white";ctx.textAlign="right";
 ctx.fillText("BEST: "+best+" m",W-25,40);
 ctx.textAlign="left";ctx.font="bold 15px Arial";
 ctx.fillText(difficulty.toUpperCase()+" MODE",W-190,70);
 ctx.fillText(MAPS[map][0],W-190,95);
}
function panel(title,sub){
 ctx.fillStyle="rgba(4,12,29,.91)";ctx.fillRect(125,65,750,475);
 ctx.strokeStyle="#50dfff";ctx.lineWidth=3;ctx.strokeRect(125,65,750,475);
 ctx.textAlign="center";
 ctx.fillStyle="#5ce5ff";ctx.font="bold 42px Arial";ctx.fillText(title,500,125);
 ctx.fillStyle="white";ctx.font="18px Arial";ctx.fillText(sub,500,160);
 ctx.textAlign="left";
}
function btn(text,x,y,w,h){
 roundedRect(x,y,w,h,9,"#145475");
 ctx.strokeStyle="#60e7ff";ctx.lineWidth=2;ctx.strokeRect(x,y,w,h);
 ctx.fillStyle="white";ctx.font="bold 17px Arial";ctx.textAlign="center";
 ctx.fillText(text,x+w/2,y+h/2+6);ctx.textAlign="left";
}
function drawMenu(){
 if(state==="menu"){
  panel("HILL CLIMB LEGENDS","Explore mountains, collect coins and drive your way to the top!");
  ctx.fillStyle="#ffe17c";ctx.font="bold 23px Arial";ctx.textAlign="center";
  ctx.fillText("YOUR COINS: "+coins,500,205);
  btn("PLAY GAME",350,235,300,55);
  btn("VEHICLE GARAGE",350,305,300,50);
  btn("MAP STORE",350,365,300,50);
  btn("DIFFICULTY: "+difficulty.toUpperCase(),350,425,300,50);
  ctx.textAlign="left";
 }else if(state==="vehicles"){
  panel("VEHICLE GARAGE","Choose your vehicle and unlock new rides!");
  ctx.fillStyle="white";ctx.font="bold 20px Arial";ctx.textAlign="center";
  ctx.fillText("YOUR COINS: "+coins,500,195);
  // Large preview
  ctx.save();ctx.translate(500,270);ctx.scale(1.5,1.5);
  ctx.fillStyle=VEHICLES[vehicle][2];
  ctx.beginPath();ctx.roundRect(-55,-15,110,30,8);ctx.fill();
  ctx.fillStyle="#9deaff";ctx.fillRect(-10,-37,35,20);
  for(let x of [-38,38]){
   ctx.fillStyle="#17202b";ctx.beginPath();ctx.arc(x,17,18,0,Math.PI*2);ctx.fill();
   ctx.strokeStyle="#bdcbd5";ctx.lineWidth=3;ctx.stroke();
  }
  ctx.fillStyle="#f3bd91";ctx.beginPath();ctx.arc(-20,-32,10,0,Math.PI*2);ctx.fill();
  ctx.restore();
  ctx.fillStyle="white";ctx.font="bold 22px Arial";
  ctx.fillText(VEHICLES[vehicle][0],500,345);
  ctx.font="17px Arial";
  ctx.fillText(ownedVehicles.includes(vehicle)?"OWNED - READY TO DRIVE":"PRICE: "+VEHICLES[vehicle][1]+" COINS",500,380);
  btn("PREVIOUS",180,410,180,50);
  btn("NEXT",640,410,180,50);
  btn(ownedVehicles.includes(vehicle)?"SELECT VEHICLE":"BUY VEHICLE",365,410,270,50);
  btn("BACK",405,480,190,42);
  ctx.textAlign="left";
 }else if(state==="maps"){
  panel("MAP STORE","Purchase a map with coins and explore new worlds!");
  for(let i=0;i<4;i++){
   let x=190+i*165;
   roundedRect(x,220,145,190,10,MAPS[i][2]);
   ctx.fillStyle=MAPS[i][3];ctx.fillRect(x,220,145,85);
   ctx.fillStyle="white";ctx.font="bold 14px Arial";ctx.textAlign="center";
   ctx.fillText(MAPS[i][0],x+72,330);
   ctx.fillText(ownedMaps.includes(i)?"OWNED":MAPS[i][1]+" COINS",x+72,360);
   ctx.fillText(i===map?"SELECTED":"CLICK TO SELECT",x+72,390);
  }
  btn("BACK",405,460,190,45);ctx.textAlign="left";
 }else if(state==="over"){
  panel("JOURNEY OVER!","Your vehicle has stopped!");
  ctx.fillStyle="white";ctx.font="bold 24px Arial";ctx.textAlign="center";
  ctx.fillText("NORMAL SCORE: "+Math.floor(score),500,220);
  ctx.fillText("BEST SCORE: "+best,500,265);
  ctx.fillText("TOTAL DISTANCE: "+Math.floor(distance)+" METERS",500,310);
  ctx.fillText("COINS COLLECTED: "+runCoins,500,355);
  btn("PLAY AGAIN",345,390,310,50);
  btn("MAIN MENU",345,455,310,45);ctx.textAlign="left";
 }else if(state==="pause"){
  panel("PAUSED","Your adventure is waiting!");
  btn("RESUME",345,260,310,55);
  btn("MAIN MENU",345,335,310,55);
 }
}
function update(dt){
 if(state!=="play")return;
 let t=dt/16.67;
 let power=VEHICLES[vehicle][3];
 let rough=difficulty==="hard"?1.45:1;

 if(gas&&fuel>0){car.vx+=.12*power*rough*t;fuel-=.055*rough*t}
 if(brake)car.vx-=.16*t;
 car.vx*=Math.pow(.995,t);
 car.vx=Math.max(-2,Math.min(8*power*rough,car.vx));
 car.x+=car.vx*t;
 car.vy+=.38*t;
 car.y+=car.vy*t;
 let gy=terrain(car.x);

 if(car.y+28>gy){
  car.y=gy-28;car.vy=0;
  let slope=(terrain(car.x+20)-terrain(car.x-20))/40;
  car.angle=Math.atan(slope);
 }
 camera=Math.max(0,car.x-260);
 distance=Math.max(distance,car.x/10);
 score=Math.floor(distance);

 wheelSpin+=car.vx*.08*t;
 let roadY=terrain(car.x);
 let bounce=roadY-(car.y+28);
 suspension+=(Math.min(8,Math.max(-8,bounce*.15))-suspension)*.15*t;

 gold.forEach(o=>{
  if(!o.taken&&Math.hypot(car.x-o.x,car.y-o.y)<48){
   o.taken=true;coins++;runCoins++;tone(850,.1,"sine",.04);save();
  }
 });
 fuelItems.forEach(o=>{
  if(!o.taken&&Math.hypot(car.x-o.x,car.y-o.y)<48){
   o.taken=true;fuel=Math.min(100,fuel+35);tone(550,.2);
  }
 });

 if(fuel<=0){
  fuel=0;car.vx*=.98;
  if(car.vx<.12)finish();
 }
 if(Math.abs(car.angle)>1.35&&Math.abs(car.vx)>1.5){
  car.rotation=(car.rotation||0)+.02*t;
 }
 if(Math.abs(car.rotation)>1.3)finish();
 if(car.y>H+100)finish();
 if(car.x>best*10){
  best=Math.floor(distance);localStorage.setItem("hlBest",best);
 }
}
function frame(now){
 let dt=Math.min(35,now-last||16);last=now;
 update(dt);background();drawGround();drawItems();
 if(state==="play"||state==="over"||state==="pause")drawCar();
 if(state==="play"||state==="over"||state==="pause")drawHUD();
 if(state!=="play")drawMenu();
 requestAnimationFrame(frame);
}
function pos(e){
 let r=c.getBoundingClientRect();
 return {x:(e.clientX-r.left)*W/r.width,y:(e.clientY-r.top)*H/r.height};
}
c.addEventListener("pointerdown",e=>{
 let p=pos(e);audioStart();
 if(state==="menu"){
  if(p.y>235&&p.y<290)startGame();
  else if(p.y>305&&p.y<355)state="vehicles";
  else if(p.y>365&&p.y<415)state="maps";
  else if(p.y>425&&p.y<480) difficulty=difficulty==="normal"?"hard":"normal";
 }else if(state==="vehicles"){
  if(p.y>410&&p.y<460){
   if(p.x<360)vehicle=(vehicle+9)%10;
   else if(p.x>640)vehicle=(vehicle+1)%10;
   else{
    if(!ownedVehicles.includes(vehicle)&&coins>=VEHICLES[vehicle][1]){
     coins-=VEHICLES[vehicle][1];ownedVehicles.push(vehicle);save();tone(900,.2);
    }else if(ownedVehicles.includes(vehicle))tone(700,.1);
    else tone(100,.2,"square");
   }
  }
  if(p.y>480&&p.y<535)state="menu";
 }else if(state==="maps"){
  if(p.y>220&&p.y<410){
   let i=Math.floor((p.x-190)/165);
   if(i>=0&&i<4){
    if(ownedMaps.includes(i))map=i;
    else if(coins>=MAPS[i][1]){
     coins-=MAPS[i][1];ownedMaps.push(i);map=i;save();tone(900,.2);
    }else tone(100,.2,"square");
   }
  }
  if(p.y>460)state="menu";
 }else if(state==="over"){
  if(p.y>390&&p.y<445)startGame();
  else if(p.y>455&&p.y<510)state="menu";
 }else if(state==="pause"){
  if(p.y>260&&p.y<320){state="play";musicStart()}
  else if(p.y>335&&p.y<400){state="menu";musicStop()}
 }
});
function hold(el,set){
 el.addEventListener("pointerdown",e=>{e.preventDefault();audioStart();set(true)});
 ["pointerup","pointerleave","pointercancel"].forEach(ev=>el.addEventListener(ev,()=>set(false)));
}
hold(document.getElementById("gas"),v=>gas=v);
hold(document.getElementById("brake"),v=>brake=v);

window.addEventListener("keydown",e=>{
 if(["ArrowRight","d","D"].includes(e.key))gas=true;
 if(["ArrowLeft","a","A"].includes(e.key))brake=true;
 if(e.key==="Escape"&&state==="play"){state="pause";musicStop()}
 if(e.code==="Space"&&state==="menu")startGame();
});
window.addEventListener("keyup",e=>{
 if(["ArrowRight","d","D"].includes(e.key))gas=false;
 if(["ArrowLeft","a","A"].includes(e.key))brake=false;
});
window.addEventListener("blur",()=>{gas=false;brake=false});
requestAnimationFrame(frame);
</script>
</body>
</html>

Game Source: Hill Climb Legends

Creator: BlazeGalaxy19

Libraries: none

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