๐ŸŽฎArcadeLab

Crafter Dash

by HyperCobra45
386 lines13.4 KB
โ–ถ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Crafter Dash</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#10182a;color:white;font-family:Arial,sans-serif;overflow:hidden}
button{border:0;border-radius:12px;padding:12px 16px;font-weight:bold;cursor:pointer;color:white;background:#6c4dff}
button:active{transform:scale(.96)}
#game{position:fixed;inset:0;width:100%;height:100%;display:block}
#hud{position:fixed;top:12px;left:12px;right:12px;display:flex;justify-content:space-between;gap:8px;pointer-events:none}
.pill{background:#111b2bdc;border:1px solid #ffffff30;border-radius:12px;padding:9px 12px;font-weight:bold}
#menu{position:fixed;inset:0;background:linear-gradient(#10182ae8,#10182af5);display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px}
.panel{width:min(520px,100%);max-height:94dvh;overflow:auto;background:#192640;border:1px solid #ffffff25;border-radius:22px;padding:22px;text-align:center;box-shadow:0 15px 50px #0008}
h1{font-size:clamp(32px,8vw,52px);margin:0;color:#ffe36e}
h2{margin:10px 0}
.sub{color:#c0cce4;line-height:1.5}
#roster{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px;margin:18px 0}
.character{background:#263753;border:2px solid transparent;border-radius:14px;padding:12px 4px;min-width:0}
.character.selected{border-color:#ffe36e;background:#374563}
.avatar{font-size:36px;display:block;margin-bottom:5px}
.name{font-size:13px;font-weight:bold;overflow-wrap:anywhere}
.cost{font-size:12px;color:#ffe36e;margin-top:6px}
#mainBtn{width:100%;font-size:18px;padding:16px;background:#24b879;margin-top:6px}
#msg{min-height:20px;color:#ffe36e;font-size:13px;margin-top:10px}
#help{font-size:12px;color:#b7c5df;line-height:1.7}
#mobile{position:fixed;bottom:20px;left:0;right:0;display:none;justify-content:space-between;padding:0 18px;pointer-events:none}
.pad{display:flex;gap:10px;pointer-events:auto}
.pad button{width:58px;height:58px;font-size:24px;background:#17243bdc;border:1px solid #ffffff45;touch-action:manipulation}
#pause{position:fixed;right:12px;top:72px;display:none;background:#17243bdc}
@media(pointer:coarse){#mobile{display:flex}}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud">
 <div class="pill">๐Ÿ† Score: <span id="score">0</span><br>โญ Best: <span id="best">0</span></div>
 <div class="pill">๐Ÿช™ <span id="coins">0</span><br><span id="runnerName">Crafter</span></div>
</div>
<button id="pause">โ…ก Pause</button>

<div id="menu">
 <div class="panel">
  <h1>CRAFTER DASH</h1>
  <p class="sub" id="subtitle">Run โ€ข Dodge โ€ข Collect โ€ข Unlock</p>
  <h2 id="sectionTitle">Choose Your Runner</h2>
  <div id="roster"></div>
  <p class="sub">๐Ÿช™ Coins unlock new characters. Choose one runner per run.</p>
  <button id="mainBtn">โ–ถ PLAY NOW</button>
  <div id="msg"></div>
  <p id="help">Computer: โ† โ†’ move ยท โ†‘ jump ยท โ†“ slide ยท P pause<br>Mobile: swipe or use the buttons below</p>
 </div>
</div>

<div id="mobile">
 <div class="pad">
  <button data-act="left">โ—€</button>
  <button data-act="right">โ–ถ</button>
 </div>
 <div class="pad">
  <button data-act="jump">โฌ†</button>
  <button data-act="slide">โฌ‡</button>
 </div>
</div>

<script>
(() => {
"use strict";

const $ = id => document.getElementById(id);
const canvas = $("game"), ctx = canvas.getContext("2d");

const characters = [
 {name:"Crafter",emoji:"๐Ÿง‘๐Ÿป",cost:0,gender:"Boy"},
 {name:"Blaze",emoji:"๐Ÿง‘๐Ÿฝ",cost:100,gender:"Boy"},
 {name:"Shadow",emoji:"๐Ÿง‘๐Ÿฟ",cost:200,gender:"Boy"},
 {name:"Neo",emoji:"๐Ÿง‘๐Ÿผ",cost:350,gender:"Boy"},
 {name:"Luna",emoji:"๐Ÿ‘ฉ๐Ÿป",cost:150,gender:"Girl"},
 {name:"Mia",emoji:"๐Ÿ‘ฉ๐Ÿฝ",cost:300,gender:"Girl"},
 {name:"Nova",emoji:"๐Ÿ‘ฉ๐Ÿฟ",cost:500,gender:"Girl"}
];

const STORE = "crafterDashSaveV1";
let save;
try {
 save = JSON.parse(localStorage.getItem(STORE)) || {};
} catch(e) { save = {}; }

let wallet = Math.max(0,Number(save.wallet)||0);
let best = Math.max(0,Number(save.best)||0);
let unlocked = Array.isArray(save.unlocked) ? save.unlocked : [0];
unlocked = [...new Set([0,...unlocked.filter(n=>Number.isInteger(n)&&n>=0&&n<characters.length)])];
let selected = 0;

let running=false, paused=false, gameOver=false;
let lane=1, jump=0, jumpV=0, slideTime=0;
let score=0, runCoins=0, distance=0, speed=0.36;
let spawnTimer=0, entities=[], lastTime=0, elapsed=0;
let W=innerWidth,H=innerHeight,DPR=1;

function persist(){
 try{
  localStorage.setItem(STORE,JSON.stringify({
   wallet,best,unlocked
  }));
 }catch(e){}
 updateHUD();
}
function updateHUD(){
 $("coins").textContent=wallet;
 $("best").textContent=best;
 $("score").textContent=score;
 $("runnerName").textContent=characters[selected].name;
}
function resize(){
 W=innerWidth;H=innerHeight;DPR=Math.min(devicePixelRatio||1,2);
 canvas.width=Math.round(W*DPR);canvas.height=Math.round(H*DPR);
 canvas.style.width=W+"px";canvas.style.height=H+"px";
 ctx.setTransform(DPR,0,0,DPR,0,0);
}
addEventListener("resize",resize);resize();

function showMenu(title,subtitle,buttonText){
 running=false;paused=false;
 $("menu").style.display="flex";
 $("sectionTitle").textContent=title;
 $("subtitle").textContent=subtitle;
 $("mainBtn").textContent=buttonText;
 $("pause").style.display="none";
 $("msg").textContent="";
 renderRoster();updateHUD();
}
function renderRoster(){
 const root=$("roster");root.innerHTML="";
 characters.forEach((c,i)=>{
  const owned=unlocked.includes(i);
  const card=document.createElement("button");
  card.className="character"+(selected===i?" selected":"");
  card.innerHTML=`<span class="avatar">${c.emoji}</span>
   <span class="name">${c.name}</span>
   <div class="cost">${i===0?"FREE":owned?"UNLOCKED":"๐Ÿช™ "+c.cost}</div>`;
  card.onclick=()=>{
   if(owned){
    selected=i;
    $("msg").textContent=c.name+" selected!";
    renderRoster();updateHUD();
   }else if(wallet>=c.cost){
    wallet-=c.cost;unlocked.push(i);selected=i;
    persist();renderRoster();
    $("msg").textContent=c.name+" unlocked! ๐ŸŽ‰";
   }else{
    $("msg").textContent="Need "+(c.cost-wallet)+" more coins to unlock "+c.name+".";
   }
  };
  root.appendChild(card);
 });
}

function startGame(){
 if(!unlocked.includes(selected))selected=0;
 lane=1;jump=0;jumpV=0;slideTime=0;
 score=0;runCoins=0;distance=0;speed=.36;
 spawnTimer=.5;entities=[];elapsed=0;
 running=true;paused=false;gameOver=false;
 $("menu").style.display="none";$("pause").style.display="block";
 updateHUD();
}
function endGame(){
 if(!running)return;
 running=false;gameOver=true;
 wallet+=runCoins;
 best=Math.max(best,score);
 persist();
 showMenu("RUN OVER!","You collected "+runCoins+" coins. Score: "+score,"โ†ป PLAY AGAIN");
}
function action(a){
 if(!running||paused)return;
 if(a==="left")lane=Math.max(0,lane-1);
 if(a==="right")lane=Math.min(2,lane+1);
 if(a==="jump"&&jump===0&&slideTime<=0)jumpV=680;
 if(a==="slide"&&jump===0)slideTime=.65;
}
addEventListener("keydown",e=>{
 if(["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"," "].includes(e.key))e.preventDefault();
 if(e.key==="ArrowLeft"||e.key.toLowerCase()==="a")action("left");
 if(e.key==="ArrowRight"||e.key.toLowerCase()==="d")action("right");
 if(e.key==="ArrowUp"||e.key===" ")action("jump");
 if(e.key==="ArrowDown")action("slide");
 if(e.key.toLowerCase()==="p"&&running){
  paused=!paused;$("pause").textContent=paused?"โ–ถ Resume":"โ…ก Pause";
 }
 if(e.key==="Enter"&&!running)startGame();
});
document.querySelectorAll("[data-act]").forEach(b=>{
 b.addEventListener("pointerdown",e=>{e.preventDefault();action(b.dataset.act)});
});
let sx=0,sy=0;
canvas.addEventListener("touchstart",e=>{
 sx=e.changedTouches[0].clientX;sy=e.changedTouches[0].clientY;
},{passive:true});
canvas.addEventListener("touchend",e=>{
 const dx=e.changedTouches[0].clientX-sx,dy=e.changedTouches[0].clientY-sy;
 if(Math.max(Math.abs(dx),Math.abs(dy))<25)return;
 if(Math.abs(dx)>Math.abs(dy))action(dx<0?"left":"right");
 else action(dy<0?"jump":"slide");
},{passive:true});

$("mainBtn").onclick=startGame;
$("pause").onclick=()=>{
 if(!running)return;
 paused=!paused;
 $("pause").textContent=paused?"โ–ถ Resume":"โ…ก Pause";
};

function spawn(){
 const laneNo=Math.floor(Math.random()*3);
 const kind=Math.random()<.57?"coin":"obstacle";
 entities.push({
  lane:laneNo,z:1,kind,
  type:kind==="coin"?"coin":
   (Math.random()<.55?"barrier":"gate"),
  taken:false
 });
}
function roadGeometry(){
 const horizon=H*.28, bottom=H*1.04;
 const roadTop=W*.11,roadBottom=W*.49;
 return {horizon,bottom,roadTop,roadBottom};
}
function perspective(z){
 const g=roadGeometry(),p=1-z;
 return {
  y:g.horizon+(g.bottom-g.horizon)*p,
  half:g.roadTop+(g.roadBottom-g.roadTop)*p,
  p
 };
}
function laneX(l,z){
 const g=roadGeometry(),p=1-z;
 const q=perspective(z);
 return W/2+(l-1)*q.half*.65;
}
function drawBackground(){
 const sky=ctx.createLinearGradient(0,0,0,H);
 sky.addColorStop(0,"#56b7ed");sky.addColorStop(.65,"#d0efff");
 sky.addColorStop(1,"#f8dca1");
 ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);

 ctx.fillStyle="#738da0";
 for(let i=0;i<13;i++){
  const x=i*W/12;
  const bh=40+(i*47%110);
  ctx.fillRect(x,H*.28-bh,W/15,bh);
  ctx.fillStyle="#d3e7ed";
  for(let j=0;j<3;j++)ctx.fillRect(x+5,H*.28-bh+10+j*15,4,5);
  ctx.fillStyle="#738da0";
 }
 const g=roadGeometry();
 ctx.fillStyle="#393f4d";
 ctx.beginPath();
 ctx.moveTo(W/2-g.roadTop,g.horizon);
 ctx.lineTo(W/2+g.roadTop,g.horizon);
 ctx.lineTo(W/2+g.roadBottom,g.bottom);
 ctx.lineTo(W/2-g.roadBottom,g.bottom);ctx.closePath();ctx.fill();

 ctx.strokeStyle="#e5e6ee";ctx.lineWidth=2;
 for(let i=1;i<=2;i++){
  ctx.beginPath();
  ctx.moveTo(W/2+(i-1)*g.roadTop-g.roadTop/3,g.horizon);
  ctx.lineTo(W/2+(i-1)*g.roadBottom-g.roadBottom/3,g.bottom);
  ctx.stroke();
 }
 // Moving road dashes
 ctx.fillStyle="#f7edba";
 for(let i=0;i<15;i++){
  const z=((i/15+distance*.018)%1);
  const q=perspective(z);
  const dashH=2+q.p*16;
  const dashW=1+q.p*5;
  for(let l=1;l<=2;l++){
   const x=W/2+(l-1)*q.half/3;
   ctx.fillRect(x-dashW/2,q.y,dashW,dashH);
  }
 }
}
function drawEntity(e){
 const q=perspective(e.z),x=laneX(e.lane,e.z);
 const scale=.2+q.p*1.25;
 if(e.kind==="coin"){
  const r=5+q.p*15;
  ctx.fillStyle="#ffc928";ctx.strokeStyle="#fff2a2";ctx.lineWidth=2;
  ctx.beginPath();ctx.ellipse(x,q.y-r,r*.78,r,0,0,Math.PI*2);ctx.fill();ctx.stroke();
  ctx.fillStyle="#9c6400";ctx.font=`bold ${Math.max(8,r)}px Arial`;
  ctx.textAlign="center";ctx.fillText("$",x,q.y+r*.35);
 }else if(e.type==="barrier"){
  const w=24+q.p*47,h=18+q.p*39;
  ctx.fillStyle="#ed5c43";ctx.fillRect(x-w/2,q.y-h,w,h);
  ctx.fillStyle="#ffe9b8";ctx.fillRect(x-w/2,q.y-h/2,w,h*.22);
 }else{
  const w=30+q.p*44,h=32+q.p*48;
  ctx.fillStyle="#7c5bdf";ctx.fillRect(x-w/2,q.y-h,w,h*.2);
  ctx.fillRect(x-w/2,q.y-h,w*.2,h);
  ctx.fillRect(x+w/2-w*.2,q.y-h,w*.2,h);
 }
}
function drawPlayer(){
 const x=laneX(lane,.045);
 const ground=H*.83;
 const lift=jump;
 const c=characters[selected];
 const y=ground-lift;
 // Shadow
 ctx.fillStyle="#0004";ctx.beginPath();
 ctx.ellipse(x,ground+8,23,7,0,0,Math.PI*2);ctx.fill();
 ctx.save();ctx.translate(x,y);
 // body and legs
 ctx.fillStyle="#263d76";ctx.fillRect(-12,-42,24,28);
 ctx.fillStyle="#ffcf9b";ctx.beginPath();ctx.arc(0,-55,15,0,Math.PI*2);ctx.fill();
 ctx.fillStyle="#272b38";ctx.beginPath();ctx.arc(0,-61,15,Math.PI,Math.PI*2);ctx.fill();
 ctx.fillStyle="#fff";ctx.font="bold 13px Arial";ctx.textAlign="center";
 ctx.fillText(c.name.slice(0,1),0,-51);
 ctx.strokeStyle="#27344e";ctx.lineWidth=7;ctx.lineCap="round";
 const swing=Math.sin(elapsed*16)*6;
 ctx.beginPath();ctx.moveTo(-6,-14);ctx.lineTo(-8+swing,-1);ctx.moveTo(6,-14);ctx.lineTo(8-swing,-1);ctx.stroke();
 ctx.restore();
}
function draw(){
 drawBackground();
 entities.slice().sort((a,b)=>a.z-b.z).forEach(drawEntity);
 drawPlayer();
 if(paused&&running){
  ctx.fillStyle="#07102099";ctx.fillRect(0,0,W,H);
  ctx.fillStyle="white";ctx.font="bold 32px Arial";ctx.textAlign="center";
  ctx.fillText("PAUSED",W/2,H/2);
 }
}
function update(dt){
 if(!running||paused)return;
 elapsed+=dt;
 speed=Math.min(.36+distance*.000035,.85);
 distance+=dt*speed*70;
 score=Math.floor(distance)+runCoins*5;
 jump=Math.max(0,jump+jumpV*dt);
 if(jumpV!==0){
  jumpV-=1650*dt;
  if(jump<=0){jump=0;jumpV=0}
 }
 slideTime=Math.max(0,slideTime-dt);
 spawnTimer-=dt;
 if(spawnTimer<=0){
  spawn();
  spawnTimer=Math.max(.36,1.0-distance*.0004)+Math.random()*.3;
 }
 for(const e of entities){
  e.z-=dt*speed;
  if(e.z<.075&&!e.taken){
   const sameLane=e.lane===lane;
   if(sameLane&&e.kind==="coin"){
    e.taken=true;runCoins++;wallet++;
    score=Math.floor(distance)+runCoins*5;
    persist();
   }else if(sameLane&&e.kind==="obstacle"){
    // Jump over low barriers; slide beneath overhead gates.
    const cleared=e.type==="barrier"?jump>45:slideTime>0;
    if(!cleared){endGame();return}
    e.taken=true;
   }else if(e.kind==="coin")e.taken=true;
  }
 }
 entities=entities.filter(e=>e.z>-.04&&!e.taken);
 updateHUD();
}
function loop(t){
 const dt=Math.min((t-lastTime)/1000||0,.035);lastTime=t;
 update(dt);draw();requestAnimationFrame(loop);
}
persist();showMenu("Choose Your Runner","Unlock all 7 characters with coins","โ–ถ PLAY NOW");
requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Game Source: Crafter Dash

Creator: HyperCobra45

Libraries: none

Complexity: complex (386 lines, 13.4 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: crafter-dash-hypercobra45" to link back to the original. Then publish at arcadelab.ai/publish.