Hill Climb Legends
by BlazeGalaxy19454 lines15.9 KB
<!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.