Horse Obby Adventure
by ChromeViper84544 lines12.3 KB
<!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.