Crafter Dash
by HyperCobra45386 lines13.4 KB
<!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.