{"project":{"id":"4DkDH7t","userId":"ferriernic@gmail.com","username":null,"userPicture":null,"name":"Donkey Kong Clone","visible":true,"contributors":"","githubRepo":null,"forkedFrom":null,"isTemplate":false,"tags":"","files":{"folder":"","files":[{"name":"index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<title>Barrel Climb</title>\n<link rel=\"stylesheet\" href=\"/css/styles.css\">\n</head>\n<body>\n  <div id=\"game-wrap\">\n    <div id=\"hud\">\n      <span id=\"score\">SCORE: 0</span>\n      <span id=\"lives\">LIVES: 3</span>\n    </div>\n    <canvas id=\"game\" width=\"672\" height=\"952\"></canvas>\n    <div id=\"msg\"></div>\n  </div>\n  <script src=\"/js/main.js\"></script>\n</body>\n</html>\n"},{"name":"README.md","content":"# Blnq Project\n"}],"folders":[{"folder":"css","files":[{"name":"styles.css","content":"html, body { height: 100%; }\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\nbody {\n  background: #0d0d1a;\n  color: #fff;\n  font-family: 'Courier New', monospace;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  height: 100vh;\n  overflow: hidden;\n}\n\n#game-wrap {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  height: 100%;\n  width: 100%;\n  justify-content: center;\n}\n\n#hud {\n  display: flex;\n  justify-content: space-between;\n  width: 100%;\n  max-width: 100%;\n  padding: 6px 12px;\n  font-size: clamp(12px, 2vw, 18px);\n  letter-spacing: 1px;\n  color: #ffcc00;\n  flex: 0 0 auto;\n}\n\ncanvas {\n  background: #1a1a2e;\n  border: 4px solid #ffcc00;\n  display: block;\n  flex: 1 1 auto;\n  min-height: 0;\n}\n\n#msg {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  font-size: clamp(16px, 3vw, 26px);\n  color: #ff4444;\n  text-align: center;\n  text-shadow: 2px 2px 0 #000;\n  pointer-events: none;\n  white-space: pre;\n}\n"}],"folders":[]},{"folder":"js","files":[{"name":"main.js","content":"// ---------- Barrel Climb: a Donkey-Kong-style platformer ----------\nconst canvas = document.getElementById('game');\nconst ctx = canvas.getContext('2d');\nconst scoreEl = document.getElementById('score');\nconst livesEl = document.getElementById('lives');\nconst msgEl = document.getElementById('msg');\n\nconst W = canvas.width, H = canvas.height;\nconst ASPECT = W / H;\n\nfunction resizeCanvas() {\n  const hud = document.getElementById('hud');\n  const hudH = hud ? hud.getBoundingClientRect().height : 0;\n  const availW = window.innerWidth - 16;\n  const availH = window.innerHeight - hudH - 16;\n  let w = availW;\n  let h = w / ASPECT;\n  if (h > availH) {\n    h = availH;\n    w = h * ASPECT;\n  }\n  canvas.style.width = w + 'px';\n  canvas.style.height = h + 'px';\n}\nwindow.addEventListener('resize', resizeCanvas);\nresizeCanvas();\n\nconst GRAVITY = 0.77;\nconst PLAT_H = 20;\nconst GAP_W = 56; // width of the ladder-gap that barrels fall through\n\n// Platform levels, bottom (index 0) to top goal (index 5).\n// Each has a gap where barrels fall through to the level below.\nconst LEVELS = [\n  { y: 896, x1: 28, x2: 644, gap: 532 },\n  { y: 756, x1: 28, x2: 644, gap: 84  },\n  { y: 616, x1: 28, x2: 644, gap: 532 },\n  { y: 476, x1: 28, x2: 644, gap: 84  },\n  { y: 336, x1: 28, x2: 644, gap: 532 },\n  { y: 224, x1: 28, x2: 644, gap: null } // goal platform, solid\n];\n\n// Ladders: connect level i to level i+1, positioned near that level's gap\nconst LADDERS = [\n  { x: 532, yTop: 756, yBot: 896 },\n  { x: 84,  yTop: 616, yBot: 756 },\n  { x: 532, yTop: 476, yBot: 616 },\n  { x: 84,  yTop: 336, yBot: 476 },\n  { x: 532, yTop: 224, yBot: 336 }\n];\n\nconst GOAL = { x: 396, y: 64, w: 220, h: 150 };\n\nlet keys = {};\nwindow.addEventListener('keydown', e => {\n  keys[e.key.toLowerCase()] = true;\n  if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(e.key.toLowerCase())) e.preventDefault();\n});\nwindow.addEventListener('keyup', e => keys[e.key.toLowerCase()] = false);\n\nfunction levelAt(y) {\n  for (let i = 0; i < LEVELS.length; i++) {\n    if (Math.abs(LEVELS[i].y - y) < 2) return i;\n  }\n  return -1;\n}\n\nfunction onLadderX(x, tolerance = 20) {\n  return LADDERS.filter(l => Math.abs(l.x - x) < tolerance);\n}\n\nclass Player {\n  constructor() {\n    this.reset();\n  }\n  reset() {\n    this.w = 28; this.h = 36;\n    this.x = 56; this.y = LEVELS[0].y - this.h;\n    this.vx = 0; this.vy = 0;\n    this.onGround = true;\n    this.climbing = false;\n    this.dead = false;\n  }\n  update() {\n    const speed = 3.6;\n    this.vx = 0;\n    if (keys['arrowleft'] || keys['a']) this.vx = -speed;\n    if (keys['arrowright'] || keys['d']) this.vx = speed;\n\n    const nearLadders = onLadderX(this.x + this.w / 2);\n    const canClimb = nearLadders.some(l => this.y + this.h >= l.yTop - 8 && this.y <= l.yBot + 8);\n\n    if ((keys['arrowup'] || keys['w']) && canClimb) {\n      this.climbing = true;\n    } else if ((keys['arrowdown'] || keys['s']) && canClimb) {\n      this.climbing = true;\n    }\n    if (this.climbing) {\n      this.vy = 0;\n      if (keys['arrowup'] || keys['w']) this.vy = -3.1;\n      if (keys['arrowdown'] || keys['s']) this.vy = 3.1;\n      if (!canClimb) this.climbing = false;\n    } else {\n      this.vy += GRAVITY;\n      if ((keys['arrowup'] || keys[' '] || keys['w']) && this.onGround) {\n        this.vy = -9.1;\n        this.onGround = false;\n      }\n    }\n\n    this.x += this.vx;\n    this.y += this.vy;\n    this.x = Math.max(28, Math.min(W - 28 - this.w, this.x));\n\n    // platform collision (only when falling / standing)\n    this.onGround = false;\n    for (const lvl of LEVELS) {\n      const top = lvl.y;\n      const withinX = this.x + this.w > lvl.x1 && this.x < lvl.x2;\n      const overGap = lvl.gap !== null && this.x + this.w > lvl.gap - 6 && this.x < lvl.gap + GAP_W + 6;\n      if (withinX && !(overGap && (keys['arrowdown'] || keys['s'])) ) {\n        if (this.vy >= 0 && this.y + this.h >= top && this.y + this.h - this.vy <= top + 1) {\n          if (!(overGap && this.climbing)) {\n            this.y = top - this.h;\n            this.vy = 0;\n            this.onGround = true;\n            this.climbing = false;\n          }\n        }\n      }\n    }\n\n    if (this.y > H) this.dead = true; // fell off bottom\n  }\n  draw() {\n    ctx.fillStyle = '#ff4d4d';\n    ctx.fillRect(this.x, this.y, this.w, this.h);\n    ctx.fillStyle = '#ffe0b3';\n    ctx.fillRect(this.x + 6, this.y + 3, this.w - 12, 11); // head\n  }\n}\n\nclass Barrel {\n  constructor(levelIndex, x, dir) {\n    this.levelIndex = levelIndex;\n    this.w = 25; this.h = 22;\n    this.x = x;\n    this.y = LEVELS[levelIndex].y - this.h;\n    this.vx = dir * 2.24;\n    this.vy = 0;\n    this.dead = false;\n    this.falling = false;\n  }\n  update() {\n    const lvl = LEVELS[this.levelIndex];\n    if (this.falling) {\n      this.vy += GRAVITY;\n      this.y += this.vy;\n      const nextLvl = LEVELS[this.levelIndex - 1];\n      if (nextLvl && this.y + this.h >= nextLvl.y) {\n        this.y = nextLvl.y - this.h;\n        this.vy = 0;\n        this.falling = false;\n        this.levelIndex -= 1;\n        this.vx = (this.x < W / 2 ? 1 : -1) * 2.24;\n      }\n      if (this.levelIndex === 0 && this.y > H) this.dead = true;\n    } else {\n      this.x += this.vx;\n      if (lvl.gap !== null && this.x + this.w / 2 > lvl.gap && this.x + this.w / 2 < lvl.gap + GAP_W) {\n        this.falling = true;\n      }\n      if (this.x < lvl.x1) { this.x = lvl.x1; this.vx *= -1; }\n      if (this.x + this.w > lvl.x2) { this.x = lvl.x2 - this.w; this.vx *= -1; }\n    }\n  }\n  draw() {\n    ctx.fillStyle = '#a0522d';\n    ctx.beginPath();\n    ctx.ellipse(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, this.h / 2, 0, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.strokeStyle = '#3d2110';\n    ctx.lineWidth = 3;\n    ctx.stroke();\n  }\n}\n\nlet player = new Player();\nlet barrels = [];\nlet score = 0;\nlet lives = 3;\nlet gameOver = false;\nlet won = false;\nlet barrelTimer = 0;\nlet barrelInterval = 130;\n\nfunction spawnBarrel() {\n  const startLevel = LEVELS.length - 2;\n  barrels.push(new Barrel(startLevel, LEVELS[startLevel].x1, 1));\n}\n\nfunction rectsOverlap(a, b) {\n  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;\n}\n\nfunction drawPlatforms() {\n  ctx.fillStyle = '#ffcc00';\n  for (const lvl of LEVELS) {\n    if (lvl.gap === null) {\n      ctx.fillRect(lvl.x1, lvl.y, lvl.x2 - lvl.x1, PLAT_H);\n    } else {\n      ctx.fillRect(lvl.x1, lvl.y, lvl.gap - lvl.x1, PLAT_H);\n      ctx.fillRect(lvl.gap + GAP_W, lvl.y, lvl.x2 - (lvl.gap + GAP_W), PLAT_H);\n    }\n  }\n  ctx.fillStyle = '#8b5a2b';\n  for (const l of LADDERS) {\n    for (let ly = l.yTop; ly < l.yBot; ly += 17) {\n      ctx.fillRect(l.x - 11, ly, 22, 6);\n    }\n    ctx.fillRect(l.x - 11, l.yTop, 4, l.yBot - l.yTop);\n    ctx.fillRect(l.x + 7, l.yTop, 4, l.yBot - l.yTop);\n  }\n}\n\nfunction drawGoal() {\n  const cx = GOAL.x + GOAL.w / 2;\n  const groundY = GOAL.y + GOAL.h;\n  const s = GOAL.h / 150;\n  const COAT = '#8a6239';\n  const COAT_DARK = '#6b4b28';\n  const LEGS = '#3a2a18';\n  const NOSE = '#1a120b';\n\n  ctx.save();\n  ctx.translate(cx, groundY);\n  ctx.scale(s, s);\n\n  // 4 short, stubby legs under the torso\n  ctx.fillStyle = LEGS;\n  const legX = [-52, -18, 16, 50];\n  for (const lx of legX) {\n    ctx.beginPath();\n    ctx.moveTo(lx - 12, -22);\n    ctx.lineTo(lx + 12, -22);\n    ctx.lineTo(lx + 12, -6);\n    ctx.quadraticCurveTo(lx + 12, 2, lx, 2);\n    ctx.quadraticCurveTo(lx - 12, 2, lx - 12, -6);\n    ctx.closePath();\n    ctx.fill();\n  }\n\n  // torso: one short, chunky circle (not stretched)\n  ctx.fillStyle = COAT;\n  ctx.beginPath();\n  ctx.arc(5, -72, 62, 0, Math.PI * 2);\n  ctx.fill();\n\n  // rump (slightly smaller circle, overlapping torso on the right)\n  ctx.beginPath();\n  ctx.arc(58, -58, 34, 0, Math.PI * 2);\n  ctx.fill();\n\n  // head: round, overlapping the front-top of the torso\n  const headCx = -78, headCy = -100;\n  ctx.beginPath();\n  ctx.arc(headCx, headCy, 46, 0, Math.PI * 2);\n  ctx.fill();\n\n  // ears: two round circles on top of the head, clearly separated\n  ctx.fillStyle = COAT_DARK;\n  ctx.beginPath();\n  ctx.arc(headCx - 30, headCy - 38, 16, 0, Math.PI * 2);\n  ctx.arc(headCx + 22, headCy - 40, 15, 0, Math.PI * 2);\n  ctx.fill();\n  ctx.fillStyle = COAT;\n  ctx.beginPath();\n  ctx.arc(headCx - 30, headCy - 36, 8, 0, Math.PI * 2);\n  ctx.arc(headCx + 22, headCy - 38, 7.5, 0, Math.PI * 2);\n  ctx.fill();\n\n  // snout: smaller circle overlapping the front of the head\n  ctx.fillStyle = COAT_DARK;\n  ctx.beginPath();\n  ctx.arc(headCx - 46, headCy + 14, 22, 0, Math.PI * 2);\n  ctx.fill();\n\n  // nose\n  ctx.fillStyle = NOSE;\n  ctx.beginPath();\n  ctx.ellipse(headCx - 64, headCy + 12, 8, 6, 0.2, 0, Math.PI * 2);\n  ctx.fill();\n\n  // eye\n  ctx.fillStyle = '#000';\n  ctx.beginPath();\n  ctx.arc(headCx - 8, headCy - 6, 4.5, 0, Math.PI * 2);\n  ctx.fill();\n\n  // subtle shading where torso meets rump\n  ctx.fillStyle = 'rgba(0,0,0,0.08)';\n  ctx.beginPath();\n  ctx.ellipse(40, -68, 20, 34, 0.3, 0, Math.PI * 2);\n  ctx.fill();\n\n  ctx.restore();\n}\n\nfunction resetGame(fullReset) {\n  player.reset();\n  barrels = [];\n  barrelTimer = 0;\n  gameOver = false;\n  won = false;\n  msgEl.textContent = '';\n  if (fullReset) { score = 0; lives = 3; }\n  updateHud();\n}\n\nfunction updateHud() {\n  scoreEl.textContent = 'SCORE: ' + score;\n  livesEl.textContent = 'LIVES: ' + lives;\n}\n\nfunction loseLife() {\n  lives -= 1;\n  updateHud();\n  if (lives <= 0) {\n    gameOver = true;\n    msgEl.textContent = 'GAME OVER\\npress R to restart';\n  } else {\n    resetGame(false);\n  }\n}\n\nfunction loop() {\n  ctx.clearRect(0, 0, W, H);\n  drawPlatforms();\n  drawGoal();\n\n  if (!gameOver && !won) {\n    player.update();\n\n    barrelTimer++;\n    if (barrelTimer > barrelInterval) {\n      spawnBarrel();\n      barrelTimer = 0;\n    }\n\n    for (const b of barrels) b.update();\n    barrels = barrels.filter(b => !b.dead);\n\n    for (const b of barrels) {\n      if (rectsOverlap(player, b)) {\n        loseLife();\n        break;\n      }\n      // jump-over scoring\n      if (!b.scored && b.x + b.w < player.x && Math.abs(b.y - player.y) < 40) {\n        b.scored = true;\n        score += 50;\n        updateHud();\n      }\n    }\n\n    if (player.dead) loseLife();\n\n    if (rectsOverlap(player, GOAL)) {\n      won = true;\n      score += 500;\n      updateHud();\n      msgEl.textContent = 'YOU WIN!\\npress R to play again';\n    }\n  }\n\n  for (const b of barrels) b.draw();\n  player.draw();\n\n  if (keys['r']) resetGame(true);\n\n  requestAnimationFrame(loop);\n}\n\nupdateHud();\nloop();\n"}],"folders":[]},{"folder":"other","files":[{"name":"main.sketch","content":""}],"folders":[],"collapsed":true}]},"variants":null,"createdAt":"2026-07-29T20:27:24.328Z","updatedAt":"2026-07-29T20:27:41.660Z","hasThumbnail":true}}