Skip to content

climb #116

@ceyhunarslans

Description

@ceyhunarslans

(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const coinsEl = document.getElementById('coins');
const levelEl = document.getElementById('level');
const leftBtn = document.getElementById('left');
const rightBtn = document.getElementById('right');
const jumpBtn = document.getElementById('jump');
const shopBtn = document.getElementById('shopBtn');
const restartBtn = document.getElementById('restartBtn');
const shopModal = document.getElementById('shopModal');
const closeShop = document.getElementById('closeShop');
const walletEl = document.getElementById('wallet');
const items = document.querySelectorAll('.item');
const ownedList = document.getElementById('ownedList');

// Responsive
function fit() {
const w = Math.min(window.innerWidth - 20, 420);
const h = Math.min(window.innerHeight - 40, 720);
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
const ratio = window.devicePixelRatio || 1;
canvas.width = Math.floor(w * ratio);
canvas.height = Math.floor(h * ratio);
ctx.setTransform(ratio,0,0,ratio,0,0);
}
fit(); window.addEventListener('resize', fit);

// Persistent data
const store = {
coins: parseInt(localStorage.getItem('cts_coins') || '0',10),
lives: parseInt(localStorage.getItem('cts_lives') || '1',10),
upgrades: JSON.parse(localStorage.getItem('cts_upgrades') || '{}'),
bestLevel: parseInt(localStorage.getItem('cts_bestLevel') || '1',10)
};

function save() {
localStorage.setItem('cts_coins', store.coins);
localStorage.setItem('cts_lives', store.lives);
localStorage.setItem('cts_upgrades', JSON.stringify(store.upgrades));
localStorage.setItem('cts_bestLevel', store.bestLevel);
}

// Game state
let levelIndex = 0;
const levels = [
{id:1, height:800, difficulty:0.9, spikes:0.02, bonus:5},
{id:2, height:900, difficulty:1.0, spikes:0.035, bonus:8},
{id:3, height:1000, difficulty:1.12, spikes:0.05, bonus:12},
{id:4, height:1100, difficulty:1.25, spikes:0.07, bonus:16},
{id:5, height:1250, difficulty:1.4, spikes:0.09, bonus:22},
{id:6, height:1400, difficulty:1.6, spikes:0.11, bonus:30},
{id:7, height:1600, difficulty:1.8, spikes:0.14, bonus:40},
{id:8, height:1800, difficulty:2.0, spikes:0.17, bonus:55},
];
const maxLevel = levels.length;

let player = {
x: 200, y: 600, w:34, h:40, vx:0, vy:0, onWall:false, canDoubleJump:false
};
let cameraY = 0;
let platforms = [];
let hazards = [];
let bonusItems = [];
let score = 0;
let running = false;
let lastTime = performance.now();

// Input
let left=false, right=false, wantJump=false;
leftBtn.addEventListener('touchstart',e=>{e.preventDefault();left=true}, {passive:false});
leftBtn.addEventListener('touchend',e=>{e.preventDefault();left=false}, {passive:false});
rightBtn.addEventListener('touchstart',e=>{e.preventDefault();right=true}, {passive:false});
rightBtn.addEventListener('touchend',e=>{e.preventDefault();right=false}, {passive:false});
jumpBtn.addEventListener('click', ()=> wantJump=true);
window.addEventListener('keydown', (e)=>{
if (e.code==='ArrowLeft') left=true;
if (e.code==='ArrowRight') right=true;
if (e.code==='Space') wantJump=true;
if (e.code==='KeyS') shopToggle();
});
window.addEventListener('keyup', (e)=>{
if (e.code==='ArrowLeft') left=false;
if (e.code==='ArrowRight') right=false;
if (e.code==='Space') wantJump=false;
});

// UI updates
function uiUpdate() {
coinsEl.textContent = Puan: ${store.coins};
levelEl.textContent = Bölüm: ${levels[levelIndex].id};
walletEl.textContent = store.coins;
// owned
ownedList.innerHTML = '';
for (const [k,v] of Object.entries(store.upgrades)) {
const li = document.createElement('li');
li.textContent = ${k} : ${v};
ownedList.appendChild(li);
}
}
uiUpdate();

// Shop
function shopToggle(){ shopModal.classList.toggle('hidden'); uiUpdate(); }
shopBtn.addEventListener('click', shopToggle);
closeShop.addEventListener('click', shopToggle);
items.forEach(btn=>{
btn.addEventListener('click', ()=>{
const id = btn.dataset.id;
const costMap = {extraLife:50, grip:120, rope:200, doubleJump:150};
const cost = costMap[id] || 9999;
if (store.coins < cost) { alert('Puanın yetmiyor'); return; }
store.coins -= cost;
if (id==='extraLife') {
store.lives = (store.lives || 0) + 1;
} else {
store.upgrades[id] = (store.upgrades[id] || 0) + 1;
}
save();
uiUpdate();
alert('Satın alındı!');
});
});

restartBtn.addEventListener('click', ()=> location.reload());

// Level generator
function genLevel(cfg) {
platforms = [];
hazards = [];
bonusItems = [];
const w = canvas.width / (window.devicePixelRatio || 1);
const h = cfg.height;
// Start platform
platforms.push({x: w/2 - 60, y: h - 40, w:120, h:18});
// generate steps upward
let y = h - 120;
while (y > 60) {
const pw = 70 + Math.random()90;
const px = Math.random()
(w - pw);
platforms.push({x: px, y: y, w: pw, h: 14});
// scatter hazards with probability
if (Math.random() < cfg.spikes) {
hazards.push({x: px + Math.random()(pw-20), y: y-10, w:18, h:18});
}
// some bonus items
if (Math.random() < 0.08) {
bonusItems.push({x: px + 10 + Math.random()
(pw-30), y: y-26, w:12, h:12, val:5 + Math.floor(Math.random()*cfg.bonus)});
}
y -= 60 + Math.random()*80;
}
// top flag
platforms.push({x: w/2 - 40, y: 30, w:80, h:18, goal:true});
}

function worldToScreen(y) {
// cameraY keeps bottom of view at cameraY (distance climbed)
const h = canvas.height / (window.devicePixelRatio || 1);
return h - (y - cameraY);
}

function rectsIntersect(a,b) {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}

function resetPlayerForLevel() {
const w = canvas.width / (window.devicePixelRatio || 1);
const cfg = levels[levelIndex];
player.x = w/2 - 10;
player.y = cfg.height - 160;
player.vx = 0; player.vy = 0;
cameraY = cfg.height - (canvas.height/(window.devicePixelRatio||1)) + 50;
score = 0;
running = true;
player.canDoubleJump = !!store.upgrades.doubleJump;
}

function startLevel(i) {
levelIndex = Math.max(0, Math.min(i, maxLevel-1));
genLevel(levels[levelIndex]);
resetPlayerForLevel();
uiUpdate();
}

// initialize
startLevel(store.bestLevel - 1 || 0);

// Game loop
function update(dt) {
if (!running) return;
const cfg = levels[levelIndex];
// input
const speed = 120 * cfg.difficulty * (store.upgrades.grip?0.95:1);
if (left) player.vx = -speed;
else if (right) player.vx = speed;
else player.vx = 0;

// physics
const gravity = 900 * cfg.difficulty;
player.vy += gravity * dt/1000;
player.x += player.vx * dt/1000;
player.y += player.vy * dt/1000;

// horizontal bounds
const w = canvas.width / (window.devicePixelRatio || 1);
if (player.x < 4) player.x = 4;
if (player.x + player.w > w-4) player.x = w - 4 - player.w;

// platform collisions (simple from above)
let onPlatform = false;
for (const p of platforms) {
  const pr = {x:p.x, y:p.y, w:p.w, h:p.h};
  // convert platform y coordinates relative to world (platform.y)
  if (player.vy >= 0 && player.x + player.w > pr.x && player.x < pr.x + pr.w) {
    // landing check: if player's feet pass through platform
    if (player.y + player.h > pr.y - 6 && player.y + player.h < pr.y + 20) {
      player.y = pr.y - player.h;
      player.vy = 0;
      onPlatform = true;
      player.canDoubleJump = !!store.upgrades.doubleJump;
      if (p.goal) {
        // level complete
        running = false;
        const reward = 30 + Math.floor(Math.random()*levels[levelIndex].bonus);
        store.coins += reward;
        store.bestLevel = Math.max(store.bestLevel, levels[levelIndex].id+1);
        save();
        alert(Bölüm tamam! +${reward} puan kazandın.);
        // advance to next level or loop
        const next = Math.min(levelIndex+1, maxLevel-1);
        startLevel(next);
        return;
      }
    }
  }
}

// hazards collision
for (let i=hazards.length-1;i>=0;i--) {
  const h = hazards[i];
  if (rectsIntersect(player, h)) {
    // if player has rope upgrade, consume and prevent death
    if (store.upgrades.rope) {
      store.upgrades.rope -= 1;
      save();
      hazards.splice(i,1);
      alert('İpin sayesinde düştün ama kurtuldun!');
      uiUpdate();
      continue;
    }
    // otherwise lose life
    store.lives = Math.max(0, (store.lives||1) - 1);
    save();
    if ((store.lives || 0) > 0) {
      // respawn on last platform
      resetPlayerForLevel();
      alert('Hasar aldın! Bir can eksildi.');
    } else {
      // game over -> lose some coins
      const lost = Math.min(store.coins, 20);
      store.coins -= lost;
      save();
      running = false;
      alert(Öldün. ${lost} puan kaybettin.);
      // restart level
      startLevel(Math.max(0, levelIndex));
    }
    uiUpdate();
    return;
  }
}

// bonus items collection
for (let i=bonusItems.length-1;i>=0;i--) {
  const b = bonusItems[i];
  if (rectsIntersect(player, b)) {
    store.coins += b.val;
    save();
    score += b.val;
    bonusItems.splice(i,1);
    uiUpdate();
  }
}

// jump handling (press to jump; if on platform -> jump; else if doubleJump -> use)
if (wantJump) {
  if (onPlatform) {
    player.vy = -520;
    onPlatform = false;
  } else if (player.canDoubleJump) {
    player.vy = -420;
    player.canDoubleJump = false;
    player.canDoubleJump = !!store.upgrades.doubleJump; // reset if still have doublejump upgrade
  }
  wantJump = false;
}

// update camera to follow player upward
const viewH = canvas.height / (window.devicePixelRatio || 1);
const topScreenY = cameraY + viewH;
// if player moves above middle of screen, move camera up
const playerWorldY = player.y + player.h;
if (playerWorldY < cameraY + viewH*0.5) {
  cameraY -= (cameraY + viewH*0.5 - playerWorldY) * 0.1;
}
// if player falls below bottom -> lose life
if (player.y > levels[levelIndex].height + 80) {
  // fell off the mountain
  store.lives = Math.max(0, (store.lives||1) - 1);
  save();
  if ((store.lives||0) > 0) {
    resetPlayerForLevel();
    alert('Düştün! Can kaybettin.');
  } else {
    const lost = Math.min(store.coins, 20);
    store.coins -= lost;
    save();
    running = false;
    alert(Düştün ve oyun bitti. ${lost} puan kaybettin.);
    startLevel(Math.max(0, levelIndex));
  }
  uiUpdate();
}

}

function draw() {
const w = canvas.width / (window.devicePixelRatio || 1);
const h = canvas.height / (window.devicePixelRatio || 1);
// background
const g = ctx.createLinearGradient(0,0,0,h);
g.addColorStop(0,'#1b3b57'); g.addColorStop(1,'#071422');
ctx.fillStyle = g;
ctx.fillRect(0,0,w,h);

// parallax distant mountains
for (let i=0;i<3;i++) {
  const mw = w*(0.7 - i*0.15);
  const mh = 120 + i*40;
  const offset = ((Date.now()/1000) * (0.5+i*0.2))%mw;
  drawMountain(-offset + i*80, h - (levels[levelIndex].height - cameraY) - 40, mw, mh);
}

// draw platforms
ctx.fillStyle = '#2e5d7a';
for (const p of platforms) {
  const sy = worldToScreen(p.y);
  ctx.fillRect(p.x, sy, p.w, p.h);
  if (p.goal) {
    // flag
    ctx.fillStyle = '#ffd166';
    ctx.fillRect(p.x + p.w - 12, sy - 20, 6, 20);
    ctx.fillStyle = '#ef476f';
    ctx.fillRect(p.x + p.w - 6, sy - 14, 18, 10);
    ctx.fillStyle = '#2e5d7a';
  }
}

// hazards
ctx.fillStyle = '#c94b4b';
for (const s of hazards) {
  const sy = worldToScreen(s.y);
  ctx.beginPath();
  ctx.moveTo(s.x, sy);
  ctx.lineTo(s.x + s.w/2, sy - s.h);
  ctx.lineTo(s.x + s.w, sy);
  ctx.closePath();
  ctx.fill();
}

// bonus
ctx.fillStyle = '#ffd166';
for (const b of bonusItems) {
  const sy = worldToScreen(b.y);
  ctx.beginPath();
  ctx.arc(b.x + b.w/2, sy + b.h/2, b.w/2, 0, Math.PI*2);
  ctx.fill();
}

// player
ctx.fillStyle = '#e6f0ff';
const py = worldToScreen(player.y);
ctx.fillRect(player.x, py, player.w, player.h);
// backpack
ctx.fillStyle = '#9bb8cc';
ctx.fillRect(player.x-6, py+8, 8, 22);

// HUD
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.fillRect(8,8,140,40);
ctx.fillStyle = '#e6f0ff';
ctx.font = '16px system-ui, -apple-system';
ctx.fillText('Puan: ' + store.coins, 16, 32);
ctx.fillText('Can: ' + (store.lives||0), 110, 32);

}

function drawMountain(x,y,w,h) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + w0.5, y - h);
ctx.lineTo(x + w, y);
ctx.closePath();
ctx.fillStyle = '#163f57';
ctx.fill();
// snow
ctx.beginPath();
ctx.moveTo(x + w
0.5, y - h);
ctx.lineTo(x + w0.6, y - h0.6);
ctx.lineTo(x + w0.4, y - h0.6);
ctx.closePath();
ctx.fillStyle = '#e6f0ff';
ctx.fill();
}

// main loop
function loop(now) {
const dt = Math.min(40, now - lastTime);
lastTime = now;
update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

})();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions