Hill Climb Adventure 2
by BlazeGalaxy19534 lines15.2 KB
<!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.