Skip to content

Commit e194ce6

Browse files
authored
Merge pull request #11 from he-is-talha/feat/add-doodle-jump-game
Feat: Add Doodle Jump Game
2 parents 7c0cd7e + f72a204 commit e194ce6

4 files changed

Lines changed: 577 additions & 0 deletions

File tree

35-Doodle-Jump-Game/index.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Talha - Doodle Jump</title>
7+
8+
<!-- 👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻-->
9+
<!-- Also uploaded the demo of this code in a gif : https://c.tenor.com/x8v1oNUOmg4AAAAd/tenor.gif-->
10+
<!-- 👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻-->
11+
12+
<!-- 👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻-->
13+
<!-- More html-css-js Games Calculators Games Cards Elements Projects on https://www.github.com/he-is-talha -->
14+
<!-- 👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻👆🏻-->
15+
16+
<link rel="icon" href="https://i.ibb.co/M6KTWnf/pic.jpg" />
17+
<link rel="stylesheet" href="style.css" />
18+
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Nunito:wght@600;800&display=swap" rel="stylesheet" />
19+
</head>
20+
<body>
21+
<div class="game-wrapper">
22+
<div class="game-ui">
23+
<span class="score-label">Score</span>
24+
<span id="scoreEl" class="score-value">0</span>
25+
<span class="high-score-label">Best</span>
26+
<span id="highScoreEl" class="high-score-value">0</span>
27+
</div>
28+
<canvas id="gameCanvas" width="480" height="600"></canvas>
29+
<div class="mobile-controls" id="mobileControls">
30+
<button type="button" class="ctrl-btn ctrl-left" id="btnLeft" aria-label="Move left"></button>
31+
<button type="button" class="ctrl-btn ctrl-right" id="btnRight" aria-label="Move right"></button>
32+
</div>
33+
<div id="startOverlay" class="overlay overlay-start">
34+
<h1>Doodle Jump</h1>
35+
<p>Use <kbd></kbd> <kbd></kbd> or <kbd>A</kbd> <kbd>D</kbd> to move</p>
36+
<p class="mobile-hint">Or tap the arrows below on mobile</p>
37+
<p>Land on platforms to jump higher!</p>
38+
<button type="button" id="startBtn" class="btn-start">Play</button>
39+
</div>
40+
<div id="gameOverOverlay" class="overlay overlay-gameover hidden">
41+
<h2>Game Over</h2>
42+
<p>Score: <span id="finalScoreEl">0</span></p>
43+
<button type="button" id="restartBtn" class="btn-start">Play Again</button>
44+
</div>
45+
</div>
46+
<script src="script.js"></script>
47+
</body>
48+
</html>

35-Doodle-Jump-Game/script.js

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
(function () {
2+
"use strict";
3+
4+
const canvas = document.getElementById("gameCanvas");
5+
const ctx = canvas.getContext("2d");
6+
if (!ctx.roundRect) {
7+
ctx.roundRect = function (x, y, w, h, r) {
8+
r = Math.min(r, w / 2, h / 2);
9+
this.beginPath();
10+
this.moveTo(x + r, y);
11+
this.lineTo(x + w - r, y);
12+
this.quadraticCurveTo(x + w, y, x + w, y + r);
13+
this.lineTo(x + w, y + h - r);
14+
this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
15+
this.lineTo(x + r, y + h);
16+
this.quadraticCurveTo(x, y + h, x, y + h - r);
17+
this.lineTo(x, y + r);
18+
this.quadraticCurveTo(x, y, x + r, y);
19+
};
20+
}
21+
const scoreEl = document.getElementById("scoreEl");
22+
const highScoreEl = document.getElementById("highScoreEl");
23+
const startOverlay = document.getElementById("startOverlay");
24+
const gameOverOverlay = document.getElementById("gameOverOverlay");
25+
const startBtn = document.getElementById("startBtn");
26+
const restartBtn = document.getElementById("restartBtn");
27+
const finalScoreEl = document.getElementById("finalScoreEl");
28+
const btnLeft = document.getElementById("btnLeft");
29+
const btnRight = document.getElementById("btnRight");
30+
31+
const CANVAS_WIDTH = 480;
32+
const CANVAS_HEIGHT = 600;
33+
const GRAVITY = 0.45;
34+
const JUMP_FORCE = -12;
35+
const MOVE_SPEED = 5;
36+
const PLATFORM_MIN_WIDTH = 60;
37+
const PLATFORM_MAX_WIDTH = 120;
38+
const PLATFORM_HEIGHT = 14;
39+
const PLATFORM_GAP_MIN = 50;
40+
const PLATFORM_GAP_MAX = 120;
41+
const PLAYER_WIDTH = 36;
42+
const PLAYER_HEIGHT = 40;
43+
const CAMERA_LEAD = 0.4;
44+
45+
let animationId = null;
46+
let player = null;
47+
let platforms = [];
48+
let cameraY = 0;
49+
let startCameraY = 0;
50+
let score = 0;
51+
let highScore = parseInt(localStorage.getItem("doodle-high-score") || "0", 10);
52+
let gameRunning = false;
53+
let keys = { left: false, right: false };
54+
let time = 0;
55+
56+
function setPixelRatio() {
57+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
58+
const rect = canvas.getBoundingClientRect();
59+
canvas.width = CANVAS_WIDTH * dpr;
60+
canvas.height = CANVAS_HEIGHT * dpr;
61+
canvas.style.width = rect.width + "px";
62+
canvas.style.height = rect.height + "px";
63+
ctx.scale(dpr, dpr);
64+
}
65+
66+
function createPlatform(x, y, width, type) {
67+
return {
68+
x,
69+
y,
70+
width,
71+
height: PLATFORM_HEIGHT,
72+
type: type || "normal",
73+
moveDir: type === "moving" ? (Math.random() > 0.5 ? 1 : -1) : 0,
74+
moveRange: type === "moving" ? 40 + Math.random() * 40 : 0,
75+
startX: x,
76+
};
77+
}
78+
79+
function initPlatforms() {
80+
platforms = [];
81+
let y = CANVAS_HEIGHT - 80;
82+
for (let i = 0; i < 10; i++) {
83+
const width =
84+
PLATFORM_MIN_WIDTH + Math.random() * (PLATFORM_MAX_WIDTH - PLATFORM_MIN_WIDTH);
85+
let x = Math.random() * (CANVAS_WIDTH - width);
86+
let type = "normal";
87+
if (i === 0) {
88+
x = (CANVAS_WIDTH - width) / 2;
89+
} else {
90+
const typeRand = Math.random();
91+
if (typeRand < 0.15) type = "break";
92+
else if (typeRand < 0.35) type = "moving";
93+
}
94+
platforms.push(createPlatform(x, y, width, type));
95+
y -= PLATFORM_GAP_MIN + Math.random() * (PLATFORM_GAP_MAX - PLATFORM_GAP_MIN);
96+
}
97+
}
98+
99+
function addPlatformsAbove(topY) {
100+
let lastY = platforms.length ? Math.min(...platforms.map((p) => p.y)) : topY;
101+
while (lastY > topY - CANVAS_HEIGHT - 200) {
102+
lastY -= PLATFORM_GAP_MIN + Math.random() * (PLATFORM_GAP_MAX - PLATFORM_GAP_MIN);
103+
const width =
104+
PLATFORM_MIN_WIDTH + Math.random() * (PLATFORM_MAX_WIDTH - PLATFORM_MIN_WIDTH);
105+
const x = Math.random() * (CANVAS_WIDTH - width);
106+
const typeRand = Math.random();
107+
let type = "normal";
108+
if (typeRand < 0.12) type = "break";
109+
else if (typeRand < 0.32) type = "moving";
110+
platforms.push(createPlatform(x, lastY, width, type));
111+
}
112+
}
113+
114+
function resetGame() {
115+
cameraY = 0;
116+
score = 0;
117+
time = 0;
118+
keys.left = false;
119+
keys.right = false;
120+
if (btnLeft) btnLeft.classList.remove("active");
121+
if (btnRight) btnRight.classList.remove("active");
122+
initPlatforms();
123+
const firstPlatform = platforms[0];
124+
player = {
125+
x: (CANVAS_WIDTH - PLAYER_WIDTH) / 2,
126+
y: firstPlatform.y - PLAYER_HEIGHT - 2,
127+
vx: 0,
128+
vy: 0,
129+
width: PLAYER_WIDTH,
130+
height: PLAYER_HEIGHT,
131+
};
132+
startCameraY = player.y - CANVAS_HEIGHT * CAMERA_LEAD;
133+
gameRunning = true;
134+
scoreEl.textContent = "0";
135+
highScoreEl.textContent = highScore;
136+
}
137+
138+
function drawPlayer(screenY) {
139+
const x = player.x;
140+
const y = player.y - cameraY;
141+
if (y < -PLAYER_HEIGHT - 20 || y > CANVAS_HEIGHT + 20) return;
142+
143+
ctx.save();
144+
ctx.translate(x + PLAYER_WIDTH / 2, y + PLAYER_HEIGHT / 2);
145+
if (keys.left) ctx.scale(-1, 1);
146+
ctx.translate(-(x + PLAYER_WIDTH / 2), -(y + PLAYER_HEIGHT / 2));
147+
148+
ctx.fillStyle = "#2d3436";
149+
ctx.beginPath();
150+
ctx.roundRect(x, y, PLAYER_WIDTH, PLAYER_HEIGHT, 8);
151+
ctx.fill();
152+
153+
ctx.fillStyle = "#fff";
154+
ctx.beginPath();
155+
ctx.arc(x + 12, y + 14, 6, 0, Math.PI * 2);
156+
ctx.arc(x + PLAYER_WIDTH - 12, y + 14, 6, 0, Math.PI * 2);
157+
ctx.fill();
158+
159+
ctx.fillStyle = "#2d3436";
160+
ctx.beginPath();
161+
ctx.arc(x + 12, y + 14, 3, 0, Math.PI * 2);
162+
ctx.arc(x + PLAYER_WIDTH - 12, y + 14, 3, 0, Math.PI * 2);
163+
ctx.fill();
164+
165+
ctx.restore();
166+
}
167+
168+
function drawPlatform(p) {
169+
const y = p.y - cameraY;
170+
if (y < -PLATFORM_HEIGHT - 20 || y > CANVAS_HEIGHT + 50) return;
171+
172+
const x = p.x;
173+
const w = p.width;
174+
const h = p.height;
175+
176+
if (p.type === "normal") {
177+
ctx.fillStyle = "#6bcb77";
178+
ctx.strokeStyle = "#4ade80";
179+
} else if (p.type === "break") {
180+
ctx.fillStyle = "#c9a959";
181+
ctx.strokeStyle = "#b8860b";
182+
} else {
183+
ctx.fillStyle = "#4d96ff";
184+
ctx.strokeStyle = "#6eb5ff";
185+
}
186+
187+
ctx.lineWidth = 2;
188+
ctx.beginPath();
189+
ctx.roundRect(x, y, w, h, 6);
190+
ctx.fill();
191+
ctx.stroke();
192+
}
193+
194+
function gameOver() {
195+
gameRunning = false;
196+
if (animationId) cancelAnimationFrame(animationId);
197+
finalScoreEl.textContent = score;
198+
gameOverOverlay.classList.remove("hidden");
199+
}
200+
201+
function gameLoop() {
202+
if (!gameRunning) return;
203+
204+
time++;
205+
const dt = 1;
206+
207+
if (keys.left) player.vx = -MOVE_SPEED;
208+
else if (keys.right) player.vx = MOVE_SPEED;
209+
else player.vx *= 0.85;
210+
player.x += player.vx;
211+
player.x = Math.max(0, Math.min(CANVAS_WIDTH - player.width, player.x));
212+
player.vy += GRAVITY;
213+
player.y += player.vy;
214+
215+
for (let i = platforms.length - 1; i >= 0; i--) {
216+
const p = platforms[i];
217+
if (p.type === "moving") {
218+
p.x = p.startX + Math.sin((time + p.startX) * 0.03) * p.moveRange * p.moveDir;
219+
p.x = Math.max(0, Math.min(CANVAS_WIDTH - p.width, p.x));
220+
}
221+
222+
const py = p.y - cameraY;
223+
if (py > CANVAS_HEIGHT + 100) {
224+
platforms.splice(i, 1);
225+
continue;
226+
}
227+
228+
const playerBottom = player.y + player.height;
229+
const platformTop = p.y;
230+
const overlapX =
231+
player.x + player.width > p.x && player.x < p.x + p.width;
232+
if (
233+
overlapX &&
234+
playerBottom >= platformTop - 2 &&
235+
playerBottom <= platformTop + 12 &&
236+
player.vy >= 0
237+
) {
238+
player.vy = JUMP_FORCE;
239+
player.y = platformTop - player.height - 1;
240+
if (p.type === "break") platforms.splice(i, 1);
241+
}
242+
}
243+
244+
const targetCameraY = player.y - CANVAS_HEIGHT * CAMERA_LEAD;
245+
if (targetCameraY < cameraY) {
246+
cameraY = targetCameraY;
247+
const newScore = Math.max(0, Math.floor((startCameraY - cameraY) / 8));
248+
if (newScore > score) {
249+
score = newScore;
250+
scoreEl.textContent = score;
251+
if (score > highScore) {
252+
highScore = score;
253+
highScoreEl.textContent = highScore;
254+
localStorage.setItem("doodle-high-score", String(highScore));
255+
}
256+
}
257+
addPlatformsAbove(cameraY);
258+
}
259+
260+
if (player.y - cameraY > CANVAS_HEIGHT + 50) {
261+
gameOver();
262+
return;
263+
}
264+
265+
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
266+
267+
platforms.forEach(drawPlatform);
268+
drawPlayer();
269+
270+
animationId = requestAnimationFrame(gameLoop);
271+
}
272+
273+
function startGame() {
274+
startOverlay.classList.add("hidden");
275+
gameOverOverlay.classList.add("hidden");
276+
resetGame();
277+
gameLoop();
278+
}
279+
280+
startBtn.addEventListener("click", startGame);
281+
restartBtn.addEventListener("click", startGame);
282+
283+
document.addEventListener("keydown", function (e) {
284+
if (e.key === "ArrowLeft" || e.key === "a" || e.key === "A") keys.left = true;
285+
if (e.key === "ArrowRight" || e.key === "d" || e.key === "D") keys.right = true;
286+
if (e.key === " ") e.preventDefault();
287+
});
288+
289+
document.addEventListener("keyup", function (e) {
290+
if (e.key === "ArrowLeft" || e.key === "a" || e.key === "A") keys.left = false;
291+
if (e.key === "ArrowRight" || e.key === "d" || e.key === "D") keys.right = false;
292+
});
293+
294+
function setKeyLeft(value) {
295+
keys.left = value;
296+
if (btnLeft) btnLeft.classList.toggle("active", value);
297+
}
298+
function setKeyRight(value) {
299+
keys.right = value;
300+
if (btnRight) btnRight.classList.toggle("active", value);
301+
}
302+
if (btnLeft) {
303+
btnLeft.addEventListener("pointerdown", function (e) {
304+
e.preventDefault();
305+
setKeyLeft(true);
306+
});
307+
btnLeft.addEventListener("pointerup", function () { setKeyLeft(false); });
308+
btnLeft.addEventListener("pointerleave", function () { setKeyLeft(false); });
309+
}
310+
if (btnRight) {
311+
btnRight.addEventListener("pointerdown", function (e) {
312+
e.preventDefault();
313+
setKeyRight(true);
314+
});
315+
btnRight.addEventListener("pointerup", function () { setKeyRight(false); });
316+
btnRight.addEventListener("pointerleave", function () { setKeyRight(false); });
317+
}
318+
319+
canvas.addEventListener("click", function () {
320+
if (gameRunning) return;
321+
if (!startOverlay.classList.contains("hidden")) startGame();
322+
});
323+
324+
window.addEventListener("resize", setPixelRatio);
325+
setPixelRatio();
326+
highScoreEl.textContent = highScore;
327+
})();

0 commit comments

Comments
 (0)