๐ŸŽฎArcadeLab

Horse Obby Adventure

by ChromeViper84
544 lines12.3 KB
โ–ถ Play

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Horse Obby Adventure</title>
<style>
* { box-sizing: border-box; }
body {
  margin: 0;
  background: #101a2b;
  color: white;
  font-family: system-ui, sans-serif;
  overflow: hidden;
}
canvas { display: block; width: 100vw; height: 100vh; }
#menu {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #101a2bdd;
  z-index: 5;
}
.panel {
  background: #1c2b43;
  border: 1px solid #40516b;
  border-radius: 22px;
  padding: 28px;
  width: min(92vw, 480px);
  text-align: center;
  box-shadow: 0 20px 70px #0008;
}
h1 { margin: 0 0 8px; font-size: 32px; }
p { color: #b9c9df; margin: 8px 0 20px; }
.horses {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin: 20px 0;
}
.horse {
  padding: 14px 5px;
  background: #263a56;
  border: 2px solid transparent;
  border-radius: 14px;
  cursor: pointer;
  color: white;
}
.horse.selected { border-color: #71e0a2; background: #315444; }
.horse span { display: block; font-size: 30px; }
button {
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
#start {
  width: 100%;
  padding: 14px;
  background: #71e0a2;
  border: 0;
  border-radius: 12px;
  color: #10251b;
  font-size: 17px;
}
#hud {
  position: fixed;
  top: 18px;
  left: 18px;
  z-index: 2;
  background: #101a2bcc;
  padding: 12px 16px;
  border-radius: 14px;
  line-height: 1.7;
  pointer-events: none;
}
#hint {
  position: fixed;
  top: 18px;
  right: 18px;
  background: #101a2bcc;
  border-radius: 12px;
  padding: 10px 14px;
  color: #d8e5f6;
  font-size: 13px;
}
#mobile {
  position: fixed;
  bottom: 22px;
  left: 0;
  right: 0;
  display: none;
  justify-content: space-between;
  padding: 0 22px;
  z-index: 3;
}
#mobile button {
  width: 66px;
  height: 66px;
  border: 0;
  border-radius: 18px;
  background: #263a56dd;
  color: white;
  font-size: 25px;
}
@media (pointer: coarse) {
  #mobile { display: flex; }
  #hint { display: none; }
}
</style>
</head>
<body>

<canvas id="game"></canvas>

<div id="hud">
  <b>๐ŸŽ HORSE OBBY</b><br>
  Coins: <span id="coins">0</span> ๐Ÿช™<br>
  Checkpoint: <span id="checkpoint">1</span>
</div>

<div id="hint">A/D or โ† โ†’ to move ยท Space to jump ยท R to restart</div>

<div id="mobile">
  <button id="left">โ—€</button>
  <button id="jump">โฌ†</button>
  <button id="right">โ–ถ</button>
</div>

<div id="menu">
  <div class="panel">
    <h1>๐Ÿด Horse Obby</h1>
    <p>Choose your horse and race through the obstacle course!</p>
    <div class="horses">
      <button class="horse selected" data-horse="0">
        <span>๐Ÿด</span>Brown
      </button>
      <button class="horse" data-horse="1">
        <span>๐Ÿฆ„</span>Unicorn
      </button>
      <button class="horse" data-horse="2">
        <span>๐ŸŽ</span>Black
      </button>
    </div>
    <button id="start">START ADVENTURE</button>
  </div>
</div>

<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

let W, H, scale;
function resize() {
  W = canvas.width = innerWidth * devicePixelRatio;
  H = canvas.height = innerHeight * devicePixelRatio;
  scale = devicePixelRatio;
  ctx.setTransform(scale, 0, 0, scale, 0, 0);
}
addEventListener("resize", resize);
resize();

const horses = [
  { name: "Brown", color: "#9b603b", mane: "#493020", speed: 5 },
  { name: "Unicorn", color: "#f3e6ff", mane: "#d47cff", speed: 5.4 },
  { name: "Black", color: "#333746", mane: "#171923", speed: 5.8 }
];

let horse = horses[0];
let horseIndex = 0;
let running = false;
let finished = false;
let x = 80, y = 0, vy = 0;
let coins = 0;
let camera = 0;
let checkpoint = 0;
let frame = 0;
let keys = {};
let collected = new Set();

const groundY = () => H / scale - 110;

const platforms = [
  { x: 0, w: 650, y: 0 },
  { x: 760, w: 420, y: -30 },
  { x: 1320, w: 450, y: 0 },
  { x: 1920, w: 450, y: -40 },
  { x: 2520, w: 480, y: 0 },
  { x: 3150, w: 500, y: -20 },
  { x: 3820, w: 600, y: 0 }
];

const obstacles = [
  { x: 340, w: 55, h: 65 },
  { x: 510, w: 65, h: 90 },
  { x: 920, w: 60, h: 70 },
  { x: 1050, w: 70, h: 100 },
  { x: 1500, w: 60, h: 85 },
  { x: 1680, w: 60, h: 110 },
  { x: 2100, w: 65, h: 80 },
  { x: 2260, w: 70, h: 110 },
  { x: 2700, w: 60, h: 85 },
  { x: 2880, w: 65, h: 110 },
  { x: 3360, w: 65, h: 95 },
  { x: 3550, w: 65, h: 120 },
  { x: 4100, w: 70, h: 100 }
];

const coinPositions = [
  210, 450, 820, 990, 1400, 1580, 2000,
  2200, 2600, 2800, 3250, 3450, 3950, 4300
];

function platformY(p) {
  return groundY() + p.y;
}

function reset() {
  x = 80;
  y = platformY(platforms[0]) - 50;
  vy = 0;
  camera = 0;
  checkpoint = 0;
  collected.clear();
  coins = 0;
  finished = false;
  document.getElementById("coins").textContent = coins;
  document.getElementById("checkpoint").textContent = 1;
}

function startGame() {
  running = true;
  document.getElementById("menu").style.display = "none";
  reset();
}

document.querySelectorAll(".horse").forEach(button => {
  button.onclick = () => {
    document.querySelectorAll(".horse").forEach(b => b.classList.remove("selected"));
    button.classList.add("selected");
    horseIndex = Number(button.dataset.horse);
    horse = horses[horseIndex];
  };
});

document.getElementById("start").onclick = startGame;

addEventListener("keydown", e => {
  keys[e.key.toLowerCase()] = true;
  if ([" ", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key))
    e.preventDefault();
  if (e.key.toLowerCase() === "r") reset();
});
addEventListener("keyup", e => keys[e.key.toLowerCase()] = false);

function holdButton(id, key) {
  const button = document.getElementById(id);
  button.addEventListener("pointerdown", e => {
    e.preventDefault();
    keys[key] = true;
  });
  for (const event of ["pointerup", "pointerleave", "pointercancel"]) {
    button.addEventListener(event, () => keys[key] = false);
  }
}
holdButton("left", "a");
holdButton("right", "d");
holdButton("jump", " ");

function drawBackground() {
  const gradient = ctx.createLinearGradient(0, 0, 0, H / scale);
  gradient.addColorStop(0, "#7ec9ff");
  gradient.addColorStop(1, "#d7f5ff");
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, W / scale, H / scale);

  // Clouds
  ctx.fillStyle = "#ffffffaa";
  for (let i = 0; i < 8; i++) {
    const cx = ((i * 230 - camera * 0.25) % (W / scale + 250) + W / scale + 250)
      % (W / scale + 250) - 100;
    const cy = 70 + (i % 3) * 65;
    ctx.beginPath();
    ctx.arc(cx, cy, 25, 0, Math.PI * 2);
    ctx.arc(cx + 30, cy - 10, 32, 0, Math.PI * 2);
    ctx.arc(cx + 65, cy, 24, 0, Math.PI * 2);
    ctx.fill();
  }

  // Distant hills
  ctx.fillStyle = "#a2e2b4";
  ctx.beginPath();
  ctx.moveTo(0, groundY());
  for (let i = 0; i <= W / scale + 100; i += 100) {
    ctx.lineTo(i, groundY() - 70 - Math.sin((i + camera * .1) / 160) * 50);
  }
  ctx.lineTo(W / scale, H / scale);
  ctx.lineTo(0, H / scale);
  ctx.fill();
}

function drawPlatform(p) {
  const sx = p.x - camera;
  const py = platformY(p);
  ctx.fillStyle = "#54b96a";
  ctx.fillRect(sx, py, p.w, 25);
  ctx.fillStyle = "#98613b";
  ctx.fillRect(sx, py + 25, p.w, H / scale - py);
  ctx.fillStyle = "#75dc7d";
  ctx.fillRect(sx, py, p.w, 8);
}

function drawObstacle(o) {
  const sx = o.x - camera;
  const py = groundY() - o.h;

  ctx.fillStyle = "#ef625b";
  ctx.fillRect(sx, py, o.w, o.h);
  ctx.fillStyle = "#ff9b79";
  ctx.fillRect(sx + 8, py + 8, o.w - 16, 10);
  ctx.fillStyle = "#bd3e46";
  ctx.fillRect(sx, py + o.h - 12, o.w, 12);
}

function drawHorse() {
  const sx = x - camera;
  const sy = y;
  const bounce = Math.sin(frame * .22) * 3;

  ctx.save();
  ctx.translate(sx, sy + bounce);

  // Tail
  ctx.strokeStyle = horse.mane;
  ctx.lineWidth = 7;
  ctx.beginPath();
  ctx.moveTo(-25, -20);
  ctx.quadraticCurveTo(-45, -35, -38, -48);
  ctx.stroke();

  // Legs
  ctx.strokeStyle = horse.color;
  ctx.lineWidth = 9;
  ctx.lineCap = "round";
  for (let i = 0; i < 4; i++) {
    const lx = -20 + i * 13;
    const leg = Math.sin(frame * .3 + i * 2) * 7;
    ctx.beginPath();
    ctx.moveTo(lx, 0);
    ctx.lineTo(lx + leg, 20);
    ctx.stroke();
  }

  // Body
  ctx.fillStyle = horse.color;
  ctx.beginPath();
  ctx.ellipse(0, -20, 32, 19, 0, 0, Math.PI * 2);
  ctx.fill();

  // Neck and head
  ctx.beginPath();
  ctx.ellipse(24, -40, 13, 24, -.35, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.ellipse(37, -57, 18, 12, -.1, 0, Math.PI * 2);
  ctx.fill();

  // Mane
  ctx.fillStyle = horse.mane;
  ctx.beginPath();
  ctx.moveTo(12, -35);
  ctx.lineTo(7, -58);
  ctx.lineTo(24, -48);
  ctx.lineTo(33, -65);
  ctx.lineTo(42, -47);
  ctx.lineTo(26, -32);
  ctx.fill();

  // Eye
  ctx.fillStyle = "#fff";
  ctx.beginPath();
  ctx.arc(45, -59, 4, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = "#171923";
  ctx.beginPath();
  ctx.arc(46, -59, 2, 0, Math.PI * 2);
  ctx.fill();

  // Rider
  ctx.fillStyle = "#315ed4";
  ctx.beginPath();
  ctx.arc(-1, -65, 10, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillRect(-10, -59, 20, 17);
  ctx.strokeStyle = "#f0c5a0";
  ctx.lineWidth = 6;
  ctx.beginPath();
  ctx.moveTo(8, -52);
  ctx.lineTo(22, -42);
  ctx.stroke();

  // Unicorn horn
  if (horseIndex === 1) {
    ctx.fillStyle = "#ffdc6e";
    ctx.beginPath();
    ctx.moveTo(43, -68);
    ctx.lineTo(48, -88);
    ctx.lineTo(51, -67);
    ctx.fill();
  }

  ctx.restore();
}

function drawCoins() {
  coinPositions.forEach((cx, i) => {
    if (collected.has(i)) return;
    const sx = cx - camera;
    const cy = groundY() - 75 - Math.sin(frame * .08 + i) * 8;

    ctx.fillStyle = "#ffcf46";
    ctx.beginPath();
    ctx.arc(sx, cy, 12, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = "#fff1a6";
    ctx.lineWidth = 3;
    ctx.stroke();
    ctx.fillStyle = "#a66b16";
    ctx.font = "bold 14px sans-serif";
    ctx.textAlign = "center";
    ctx.fillText("$", sx, cy + 5);
  });
}

function update() {
  if (!running || finished) return;

  frame++;

  const speed = horse.speed;
  if (keys.a || keys.arrowleft) x -= speed;
  if (keys.d || keys.arrowright) x += speed;

  x = Math.max(0, x);

  if ((keys[" "] || keys.arrowup || keys.w) && vy === 0) {
    vy = -13;
  }

  vy += .65;
  y += vy;

  // Landing
  let onGround = false;
  for (const p of platforms) {
    const py = platformY(p);
    if (x + 20 > p.x && x - 20 < p.x + p.w &&
        y + 5 >= py && y + 5 <= py + 25 && vy >= 0) {
      y = py - 50;
      vy = 0;
      onGround = true;
    }
  }

  if (!onGround && y > H / scale + 100) {
    x = Math.max(80, checkpoint * 650 + 80);
    y = platformY(platforms[0]) - 50;
    vy = 0;
  }

  // Checkpoint progression
  if (x > 700) checkpoint = Math.max(checkpoint, 1);
  if (x > 1300) checkpoint = Math.max(checkpoint, 2);
  if (x > 1900) checkpoint = Math.max(checkpoint, 3);
  if (x > 2500) checkpoint = Math.max(checkpoint, 4);
  if (x > 3100) checkpoint = Math.max(checkpoint, 5);
  document.getElementById("checkpoint").textContent = checkpoint + 1;

  // Obstacles
  for (const o of obstacles) {
    if (x + 22 > o.x && x - 22 < o.x + o.w &&
        y + 5 > groundY() - o.h) {
      x = Math.max(80, checkpoint * 650 + 80);
      y = platformY(platforms[0]) - 50;
      vy = 0;
    }
  }

  // Collect coins
  coinPositions.forEach((cx, i) => {
    if (!collected.has(i) && Math.abs(x - cx) < 35 &&
        Math.abs(y + 25 - (groundY() - 75)) < 65) {
      collected.add(i);
      coins++;
      document.getElementById("coins").textContent = coins;
    }
  });

  // Finish
  if (x > 4350) {
    finished = true;
    document.getElementById("menu").style.display = "flex";
    document.querySelector(".panel h1").textContent = "๐Ÿ† You Win!";
    document.querySelector(".panel p").textContent =
      "You finished the obby with " + coins + " coins!";
    document.getElementById("start").textContent = "PLAY AGAIN";
  }

  camera = Math.max(0, x - W / scale * .35);
}

function draw() {
  drawBackground();

  platforms.forEach(drawPlatform);
  drawCoins();
  obstacles.forEach(drawObstacle);

  // Finish flag
  const finishX = 4400 - camera;
  ctx.fillStyle = "#fff";
  ctx.fillRect(finishX, groundY() - 150, 8, 150);
  ctx.fillStyle = "#ffcf46";
  ctx.beginPath();
  ctx.moveTo(finishX + 8, groundY() - 150);
  ctx.lineTo(finishX + 65, groundY() - 125);
  ctx.lineTo(finishX + 8, groundY() - 100);
  ctx.fill();

  if (running) drawHorse();
}

function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>

Game Source: Horse Obby Adventure

Creator: ChromeViper84

Libraries: none

Complexity: complex (544 lines, 12.3 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: horse-obby-adventure-chromeviper84" to link back to the original. Then publish at arcadelab.ai/publish.