Subway Runner
by LaserLegend64532 lines14.6 KB
<!DOCTYPE html>
<html lang="fa">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="theme-color" content="#0b1020">
<title>Subway Runner</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;}
html,body{
height:100%;overflow:hidden;background:#0b1020;
font-family:system-ui,-apple-system,"Segoe UI",sans-serif;
touch-action:none;-webkit-user-select:none;user-select:none;
-webkit-tap-highlight-color:transparent;
}
canvas{display:block;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(function(){
'use strict';
/* ================== SETUP ================== */
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d', { alpha:false });
let W=0, H=0, DPR=1, HORIZON=0, focal=0;
const CAM_Z = -6, CAM_Y = 2.3;
function resize(){
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
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);
HORIZON = H * 0.40;
focal = H * 0.92;
}
resize();
window.addEventListener('resize', resize);
window.addEventListener('orientationchange', ()=>setTimeout(resize,200));
/* ================== PROJECTION ================== */
function project(x, y, z){
const dz = z - CAM_Z;
if (dz < 0.7) return null;
const s = focal / dz;
return { x: W*0.5 + x*s, y: HORIZON + (CAM_Y - y)*s, s:s };
}
/* ================== BOX RENDERER ================== */
function drawBox(x, y, z, w, h, d, hue, sat, lit){
const hw=w/2, hd=d/2;
const zn = z-hd, zf = z+hd;
const yb = y, yt = y+h;
const A = project(x-hw, yb, zn);
const B = project(x+hw, yb, zn);
const C = project(x+hw, yt, zn);
const D = project(x-hw, yt, zn);
const E = project(x-hw, yb, zf);
const F = project(x+hw, yb, zf);
const G = project(x+hw, yt, zf);
const I = project(x-hw, yt, zf);
if(!A||!B||!C||!D||!E||!F||!G||!I) return;
// face side
if (x > 0.02){
ctx.fillStyle = `hsl(${hue},${sat}%,${lit*0.62}%)`;
ctx.beginPath();
ctx.moveTo(A.x,A.y); ctx.lineTo(D.x,D.y);
ctx.lineTo(I.x,I.y); ctx.lineTo(E.x,E.y);
ctx.closePath(); ctx.fill();
} else if (x < -0.02){
ctx.fillStyle = `hsl(${hue},${sat}%,${lit*0.62}%)`;
ctx.beginPath();
ctx.moveTo(B.x,B.y); ctx.lineTo(C.x,C.y);
ctx.lineTo(G.x,G.y); ctx.lineTo(F.x,F.y);
ctx.closePath(); ctx.fill();
}
// face top
if (yt < CAM_Y - 0.05){
ctx.fillStyle = `hsl(${hue},${sat}%,${Math.min(88, lit*1.42)}%)`;
ctx.beginPath();
ctx.moveTo(D.x,D.y); ctx.lineTo(C.x,C.y);
ctx.lineTo(G.x,G.y); ctx.lineTo(I.x,I.y);
ctx.closePath(); ctx.fill();
}
// face front
ctx.fillStyle = `hsl(${hue},${sat}%,${lit}%)`;
ctx.beginPath();
ctx.moveTo(A.x,A.y); ctx.lineTo(B.x,B.y);
ctx.lineTo(C.x,C.y); ctx.lineTo(D.x,D.y);
ctx.closePath(); ctx.fill();
}
/* ================== GROUND QUAD ================== */
function groundQuad(x1,x2,z1,z2,color){
const a = project(x1,0,z1), b = project(x2,0,z1);
const c = project(x2,0,z2), d = project(x1,0,z2);
if(!a||!b||!c||!d) return;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y);
ctx.lineTo(c.x,c.y); ctx.lineTo(d.x,d.y);
ctx.closePath(); ctx.fill();
}
/* ================== GAME STATE ================== */
const LANE_X = [-1.65, 0, 1.65];
const GRAV = -26;
const JUMP_V = 8.2;
const SLIDE_T = 0.72;
let player, items, speed, distance, score, coins, best;
let gameOver, spawnAccum, gap, time, shakeT, lastT;
best = parseInt(localStorage.getItem('sr_best') || '0', 10) || 0;
function reset(){
player = { lane:1, x:0, y:0, vy:0, sliding:false, slideT:0 };
items = [];
speed = 14;
distance = 0;
score = 0;
coins = 0;
gameOver = false;
spawnAccum = 0;
gap = 18;
time = 0;
shakeT = 0;
lastT = performance.now();
}
/* ================== SPAWNING ================== */
function addBarrier(lane, z){
items.push({
type:'barrier', x:LANE_X[lane], y:0, z:z,
w:1.25, h:0.85, d:1.0, hue:28, sat:85, lit:55
});
}
function addGate(lane, z){
items.push({
type:'gate', x:LANE_X[lane], y:1.15, z:z,
w:1.35, h:1.05, d:0.7, hue:352, sat:75, lit:52
});
}
function addTrain(lane, z){
const hue = [205, 268, 320][Math.floor(Math.random()*3)];
items.push({
type:'train', x:LANE_X[lane], y:0, z:z+3.2,
w:1.42, h:2.15, d:8.0, hue:hue, sat:60, lit:48
});
}
function addCoin(lane, z){
items.push({
type:'coin', x:LANE_X[lane], y:0.75, z:z,
w:0.6, h:0.6, d:0.6, seed: Math.random()*6.28
});
}
function spawnRow(z){
const lanes = [0,1,2];
for(let i=lanes.length-1;i>0;i--){
const j = Math.floor(Math.random()*(i+1));
const t = lanes[i]; lanes[i]=lanes[j]; lanes[j]=t;
}
const two = Math.random() < 0.32;
const nBlock = two ? 2 : 1;
let hasTrain = false;
for(let i=0;i<nBlock;i++){
const l = lanes[i];
// اگه دو خط بستهست، قطار نمیذاریم تا راه فرار بمونه
const r = two ? Math.random()*0.9 : Math.random();
if (r < 0.38) addBarrier(l, z);
else if (r < 0.72) addGate(l, z);
else { addTrain(l, z); hasTrain = true; }
}
const free = lanes.slice(nBlock);
if (free.length && Math.random() < 0.8){
const l = free[Math.floor(Math.random()*free.length)];
const n = 3 + Math.floor(Math.random()*4);
for(let i=0;i<n;i++) addCoin(l, z + i*1.4);
}
return hasTrain;
}
/* ================== CONTROLS ================== */
function moveLeft(){ if(!gameOver && player.lane > 0) player.lane--; }
function moveRight(){ if(!gameOver && player.lane < 2) player.lane++; }
function jump(){
if(gameOver) return;
if(player.y <= 0.02){
player.vy = JUMP_V;
player.sliding = false;
player.slideT = 0;
}
}
function slide(){
if(gameOver) return;
player.sliding = true;
player.slideT = SLIDE_T;
if(player.y > 0.02) player.vy = Math.min(player.vy, -14);
}
window.addEventListener('keydown', e=>{
const k = e.key;
if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].includes(k)) e.preventDefault();
if(gameOver){
if(k===' '||k==='Enter'||k==='ArrowUp') reset();
return;
}
if(k==='ArrowLeft' || k==='a' || k==='A') moveLeft();
else if(k==='ArrowRight' || k==='d' || k==='D') moveRight();
else if(k==='ArrowUp' || k==='w' || k==='W' || k===' ') jump();
else if(k==='ArrowDown' || k==='s' || k==='S') slide();
});
let ptr = null;
canvas.addEventListener('pointerdown', e=>{
ptr = { x:e.clientX, y:e.clientY, t:performance.now() };
canvas.setPointerCapture(e.pointerId);
});
canvas.addEventListener('pointerup', e=>{
if(!ptr) return;
const dx = e.clientX - ptr.x;
const dy = e.clientY - ptr.y;
const ax = Math.abs(dx), ay = Math.abs(dy);
ptr = null;
if(gameOver){ reset(); return; }
if(Math.max(ax,ay) < 28){ jump(); return; }
if(ax > ay) (dx > 0) ? moveRight() : moveLeft();
else (dy > 0) ? slide() : jump();
});
canvas.addEventListener('pointercancel', ()=>{ ptr=null; });
canvas.addEventListener('contextmenu', e=>e.preventDefault());
/* ================== UPDATE ================== */
function update(dt){
if(gameOver) return;
time += dt;
speed = Math.min(34, 14 + distance * 0.011);
distance += speed * dt;
score = Math.floor(distance) + coins * 10;
// حرکت افقی بازیکن
const tx = LANE_X[player.lane];
player.x += (tx - player.x) * Math.min(1, dt * 16);
// پرش
if(player.y > 0 || player.vy !== 0){
player.vy += GRAV * dt;
player.y += player.vy * dt;
if(player.y <= 0){ player.y = 0; player.vy = 0; }
}
// سُر خوردن
if(player.sliding){
player.slideT -= dt;
if(player.slideT <= 0) player.sliding = false;
}
// spawn
spawnAccum += speed * dt;
if(spawnAccum >= gap){
spawnAccum -= gap;
const hasTrain = spawnRow(95);
gap = 12 + speed * 0.42 + (hasTrain ? 7 : 0);
}
// برخوردها
const ph = player.sliding ? 0.72 : 1.35;
const pw = 0.72, pd = 0.5;
for(let i=items.length-1; i>=0; i--){
const it = items[i];
const pz = it.z;
it.z -= speed * dt;
if(it.z < -12){ items.splice(i,1); continue; }
const tz = (it.d + pd) * 0.5;
if(pz >= -tz && it.z <= tz){
const tx = (it.w + pw) * 0.5;
if(Math.abs(it.x - player.x) < tx){
if(it.type === 'coin'){
coins++;
items.splice(i,1);
continue;
}
const yHit = !(it.y + it.h < player.y || it.y > player.y + ph);
if(yHit){
gameOver = true;
shakeT = 0.45;
if(score > best){
best = score;
localStorage.setItem('sr_best', String(best));
}
}
}
}
}
}
/* ================== RENDER ================== */
function drawPlayer(){
const ph = player.sliding ? 0.72 : 1.35;
const pw = 0.82, pd = 0.5;
// سایه
const g = project(player.x, 0, 0);
if(g){
ctx.save();
ctx.globalAlpha = 0.38 - Math.min(0.25, player.y*0.15);
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.ellipse(g.x, g.y, 0.55*g.s, 0.20*g.s, 0, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
// بدن
drawBox(player.x, player.y, 0, pw, ph, pd, 198, 82, 55);
// سر
if(player.sliding){
drawBox(player.x, player.y + 0.30, 0.42, 0.42, 0.40, 0.40, 32, 78, 66);
} else {
drawBox(player.x, player.y + ph, 0, 0.48, 0.44, 0.44, 32, 78, 66);
// کلاه
drawBox(player.x, player.y + ph + 0.34, 0, 0.52, 0.10, 0.48, 210, 75, 45);
}
}
function drawCoin(it){
const p = project(it.x, it.y, it.z);
if(!p) return;
const baseR = 0.30 * p.s;
const ph = Math.abs(Math.cos(time*6 + it.seed));
const rx = Math.max(1, baseR * (0.22 + 0.78*ph));
const ry = Math.max(1, baseR);
const grad = ctx.createLinearGradient(p.x-rx, p.y-ry, p.x+rx, p.y+ry);
grad.addColorStop(0, '#fff2a8');
grad.addColorStop(0.45,'#f7c500');
grad.addColorStop(1, '#a86e00');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(p.x, p.y, rx, ry, 0, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.55)';
ctx.lineWidth = Math.max(0.8, p.s*0.02);
ctx.stroke();
}
function drawItem(it){
if(it.type === 'coin'){ drawCoin(it); return; }
drawBox(it.x, it.y, it.z, it.w, it.h, it.d, it.hue, it.sat, it.lit);
}
function drawScene(){
// آسمان
const sky = ctx.createLinearGradient(0,0,0,HORIZON+30);
sky.addColorStop(0, '#080c1c');
sky.addColorStop(0.55,'#1a1436');
sky.addColorStop(1, '#3a2260');
ctx.fillStyle = sky;
ctx.fillRect(0,0,W,HORIZON+30);
// زمین
ctx.fillStyle = '#141a2e';
ctx.fillRect(0,HORIZON,W,H-HORIZON);
// خطوط مسیر
const bounds = [-2.45,-0.82,0.82,2.45];
for(const bx of bounds){
groundQuad(bx-0.045, bx+0.045, -4, 100, 'rgba(255,255,255,0.07)');
}
// نوارهای متحرک سرعت
const SP = 5;
const off = distance % SP;
for(let i=0;i<26;i++){
const z = i*SP - off - 4;
if(z < -4) continue;
groundQuad(-2.45, 2.45, z, z+0.22, 'rgba(255,255,255,0.045)');
}
// مه افق
const fog = ctx.createLinearGradient(0,HORIZON-4,0,HORIZON+H*0.14);
fog.addColorStop(0,'rgba(58,34,96,0.95)');
fog.addColorStop(1,'rgba(58,34,96,0)');
ctx.fillStyle = fog;
ctx.fillRect(0,HORIZON-4,W,H*0.14+4);
// آیتمها: دور به نزدیک
items.sort((a,b)=> b.z - a.z);
let i = 0;
for(; i<items.length; i++){
if(items[i].z < 0) break;
drawItem(items[i]);
}
// بازیکن
drawPlayer();
// آیتمهای پشت بازیکن
for(; i<items.length; i++){
drawItem(items[i]);
}
}
function drawHUD(){
const pad = Math.max(14, W*0.04);
ctx.textBaseline = 'top';
// امتیاز
ctx.font = '700 ' + Math.round(H*0.032) + 'px system-ui, sans-serif';
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.fillText(String(score), pad+2, pad+2);
ctx.fillStyle = '#ffffff';
ctx.fillText(String(score), pad, pad);
// سکهها
const cR = H*0.016;
const cx = W - pad - cR;
const cy = pad + cR;
const grad = ctx.createLinearGradient(cx-cR,cy-cR,cx+cR,cy+cR);
grad.addColorStop(0,'#fff2a8');
grad.addColorStop(0.5,'#f7c500');
grad.addColorStop(1,'#a86e00');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(cx, cy, cR, 0, Math.PI*2);
ctx.fill();
ctx.font = '700 ' + Math.round(H*0.03) + 'px system-ui, sans-serif';
ctx.textAlign = 'right';
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.fillText(String(coins), W-pad-2*cR-10+2, pad+2);
ctx.fillStyle = '#ffffff';
ctx.fillText(String(coins), W-pad-2*cR-10, pad);
// بهترین رکورد
if(best > 0){
ctx.textAlign = 'center';
ctx.font = '600 ' + Math.round(H*0.019) + 'px system-ui, sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.42)';
ctx.fillText('BEST ' + best, W*0.5, pad + H*0.045);
}
}
function drawGameOver(){
ctx.fillStyle = 'rgba(6,9,20,0.78)';
ctx.fillRect(0,0,W,H);
const cx = W*0.5, cy = H*0.5;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#ff5c7a';
ctx.font = '800 ' + Math.round(H*0.062) + 'px system-ui, sans-serif';
ctx.fillText('GAME OVER', cx, cy - H*0.13);
ctx.fillStyle = '#ffffff';
ctx.font = '700 ' + Math.round(H*0.05) + 'px system-ui, sans-serif';
ctx.fillText(String(score), cx, cy - H*0.035);
ctx.fillStyle = 'rgba(255,255,255,0.55)';
ctx.font = '600 ' + Math.round(H*0.022) + 'px system-ui, sans-serif';
ctx.fillText('امتیاز', cx, cy + H*0.015);
ctx.fillStyle = '#f7c500';
ctx.font = '700 ' + Math.round(H*0.03) + 'px system-ui, sans-serif';
ctx.fillText('🪙 ' + coins + ' 🏆 ' + best, cx, cy + H*0.075);
const pulse = 0.6 + 0.4*Math.sin(time*4);
ctx.fillStyle = 'rgba(255,255,255,' + pulse.toFixed(2) + ')';
ctx.font = '600 ' + Math.round(H*0.026) + 'px system-ui, sans-serif';
ctx.fillText('برای شروع دوباره ضربه بزن', cx, cy + H*0.15);
// راهنما
ctx.fillStyle = 'rgba(255,255,255,0.30)';
ctx.font = '500 ' + Math.round(H*0.018) + 'px system-ui, sans-serif';
ctx.fillText('◀ ▶ تغییر خط • ▲ پرش • ▼ سُر خوردن', cx, H - H*0.08);
}
function render(){
ctx.save();
if(shakeT > 0){
const k = shakeT * 22;
ctx.translate((Math.random()-0.5)*k, (Math.random()-0.5)*k);
}
drawScene();
ctx.restore();
drawHUD();
if(gameOver) drawGameOver();
}
/* ================== LOOP ================== */
function frame(now){
let dt = (now - lastT) / 1000;
lastT = now;
if(dt > 0.05) dt = 0.05;
if(shakeT > 0) shakeT -= dt;
update(dt);
render();
requestAnimationFrame(frame);
}
/* ================== START ================== */
reset();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Game Source: Subway Runner
Creator: LaserLegend64
Libraries: none
Complexity: complex (532 lines, 14.6 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: subway-runner-laserlegend64" to link back to the original. Then publish at arcadelab.ai/publish.