1/* ============================================================ 2 JOUST â flap your ostrich-knight, joust enemies by colliding 3 with a HIGHER lance. Defeated foes drop eggs that hatch if 4 not collected. Lava hands grab the fallen. Waves clear when 5 all enemies are defeated. 2-button feel: flap + gravity. 6 ============================================================ */ 7"use strict"; 8 9(function () { 10 const LW = 672, LH = 768; 11 const GRAV = 620, FLAP_V = -235, MAX_FALL = 380; 12 const GROUND_Y = 700; 13 /* platforms */ 14 const PLATS = [ 15 { x: 0, y: 620, w: 150 }, 16 { x: 250, y: 560, w: 172 }, 17 { x: 522, y: 620, w: 150 }, 18 { x: 90, y: 430, w: 150 }, 19 { x: 432, y: 430, w: 150 }, 20 { x: 250, y: 300, w: 172 }, 21 { x: 0, y: 200, w: 120 }, 22 { x: 552, y: 200, w: 120 }, 23 ]; 24 25 function makeGame(ctx) { 26 const { g, sound, input, saveScore } = ctx; 27 28 let state, score, lives, wave; 29 let player, enemies, eggs, particles, lava, lavaT, msg, msgT; 30 const rnd = (a, b) => a + Math.random() * (b - a); 31 32 function newPlayer() { 33 return { x: LW / 2, y: 560, vx: 0, vy: 0, dir: 1, onGround: false, flapT: 0, dead: false }; 34 } 35 36 function spawnWave() { 37 enemies = []; 38 const n = 2 + Math.min(wave, 5); 39 const types = ["bounder", "hunter", "shadow"]; 40 for (let i = 0; i < n; i++) { 41 const t = wave < 2 ? "bounder" : types[i % 3]; 42 const p = PLATS[(Math.random() * PLATS.length) | 0]; 43 enemies.push({ 44 type: t, x: p.x + p.w / 2, y: p.y - 30, 45 vx: rnd(-60, 60), vy: 0, dir: 1, flapT: rnd(0, 1), 46 speed: t === "shadow" ? 150 : t === "hunter" ? 120 : 90, 47 aggr: t === "hunter" ? 0.7 : t === "shadow" ? 0.5 : 0.2, 48 }); 49 } 50 eggs = []; 51 msg = "WAVE " + wave; msgT = 1.4; 52 } 53 54 function init() { 55 state = "play"; score = 0; lives = 3; wave = 1; 56 player = newPlayer(); 57 particles = []; 58 lava = { active: false, t: 0 }; 59 lavaT = 12; 60 spawnWave(); 61 } 62 63 function burst(x, y, n, col) { 64 for (let i = 0; i < n; i++) { 65 const a = rnd(0, Math.PI * 2), sp = rnd(50, 220); 66 particles.push({ x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, life: rnd(0.3, 0.6), col, size: rnd(2, 5) }); 67 } 68 } 69 70 function flap(e) { 71 e.vy = FLAP_V; 72 e.flapT = 0.25; 73 sound.tone(360, 0.08, { type: "square", vol: 0.1, slide: 520 }); 74 } 75 76 function killPlayer() { 77 if (player.dead) return; 78 player.dead = true; 79 burst(player.x, player.y, 22, "#39e0ff"); 80 sound.noise(0.4, 0.3); 81 for (let i = 0; i < 5; i++) sound.tone(500 - i * 70, 0.12, { type: "triangle", vol: 0.2, delay: i * 0.09 }); 82 lives--; 83 if (lives <= 0) { state = "over"; saveScore(score); } 84 else setTimeout(() => { if (state === "play") player = newPlayer(); }, 900); 85 } 86 87 function physics(e, dt, isPlayer) { 88 e.vy += GRAV * dt; 89 if (e.vy > MAX_FALL) e.vy = MAX_FALL; 90 e.x += e.vx * dt; 91 e.y += e.vy * dt; 92 if (e.flapT > 0) e.flapT -= dt; 93 94 /* screen wrap horizontally */ 95 if (e.x < -20) e.x = LW + 20; 96 if (e.x > LW + 20) e.x = -20; 97 98 /* platform landing (only when falling) */ 99 e.onGround = false; 100 if (e.vy >= 0) { 101 for (const p of PLATS) { 102 if (e.x > p.x - 6 && e.x < p.x + p.w + 6 && e.y > p.y - 30 && e.y < p.y + 8 && e.y + e.vy * dt >= p.y - 26) { 103 e.y = p.y - 26; e.vy = 0; e.onGround = true; break; 104 } 105 } 106 /* ground */ 107 if (e.y > GROUND_Y - 26) { e.y = GROUND_Y - 26; e.vy = 0; e.onGround = true; } 108 } 109 } 110 111 function update(dt) { 112 if (msgT > 0) msgT -= dt; 113 for (const p of particles) { p.x += p.vx * dt; p.y += p.vy * dt; p.vy += 200 * dt; p.life -= dt; } 114 particles = particles.filter((p) => p.life > 0); 115 if (state !== "play") return; 116 117 /* player input */ 118 if (!player.dead) { 119 let mv = 0; 120 if (input.held.has("left")) mv -= 1; 121 if (input.held.has("right")) mv += 1; 122 if (mv !== 0) { player.dir = mv; player.vx += mv * 600 * dt; } 123 else player.vx *= 0.9; 124 player.vx = Math.max(-200, Math.min(200, player.vx)); 125 if (input.held.has("fire") || input.held.has("up")) { 126 if (!player._flapHeld) { flap(player); player._flapHeld = true; } 127 } else player._flapHeld = false;
128 physics(player, dt, true); 129 } 130 131 /* enemies AI */ 132 for (const e of enemies) { 133 e.flapT -= dt; 134 /* seek player horizontally with some aggression */ 135 if (!player.dead && Math.random() < e.aggr * dt * 8) { 136 e.dir = player.x > e.x ? 1 : -1; 137 } 138 e.vx += e.dir * e.speed * dt * 2; 139 e.vx = Math.max(-e.speed, Math.min(e.speed, e.vx)); 140 /* periodic flap to stay aloft */ 141 if (e.flapT <= 0 && (e.onGround || e.vy > 40)) { flap(e); e.flapT = rnd(0.4, 1.0); } 142 physics(e, dt, false); 143 144 /* collision with player */ 145 if (!player.dead && Math.abs(e.x - player.x) < 26 && Math.abs(e.y - player.y) < 26) { 146 if (player.y < e.y - 4) { 147 /* player wins â higher lance */ 148 e.dead = true; 149 score += e.type === "shadow" ? 750 : e.type === "hunter" ? 500 : 250; 150 burst(e.x, e.y, 14, "#ffe600"); 151 eggs.push({ x: e.x, y: e.y, vy: -120, vx: e.vx * 0.4, hatchT: 6, onGround: false }); 152 sound.tone(700, 0.12, { vol: 0.18, slide: 200 }); 153 player.vy = FLAP_V * 0.7; /* bounce */ 154 } else if (player.y > e.y + 4) { 155 killPlayer(); 156 } 157 } 158 } 159 enemies = enemies.filter((e) => !e.dead); 160 161 /* eggs: fall, land, hatch into bounder if not collected */ 162 for (const eg of eggs) { 163 if (eg.collected) continue; 164 eg.vy += GRAV * dt; 165 eg.x += eg.vx * dt; eg.y += eg.vy * dt; 166 eg.onGround = false; 167 if (eg.vy >= 0) { 168 for (const p of PLATS) { 169 if (eg.x > p.x && eg.x < p.x + p.w && eg.y > p.y - 12 && eg.y < p.y + 8) { eg.y = p.y - 10; eg.vy = 0; eg.vx = 0;
169 eg.onGround = true; break; } 170 } 171 if (eg.y > GROUND_Y - 10) { eg.y = GROUND_Y - 10; eg.vy = 0; eg.vx = 0; eg.onGround = true; } 172 } 173 if (eg.onGround) { 174 eg.hatchT -= dt; 175 if (eg.hatchT <= 0) { 176 eg.collected = true; 177 enemies.push({ type: "bounder", x: eg.x, y: eg.y - 16, vx: rnd(-60, 60), vy: -100, dir: 1, flapT: 0.3, speed: 95, aggr: 0.3 }); 178 burst(eg.x, eg.y, 8, "#ff9d2e"); 179 } 180 } 181 /* player collects egg */ 182 if (!player.dead && Math.abs(eg.x - player.x) < 24 && Math.abs(eg.y - player.y) < 24) { 183 eg.collected = true; score += 250; 184 burst(eg.x, eg.y, 10, "#39ff7e"); 185 sound.tone(880, 0.08, { vol: 0.16, slide: 1200 }); 186 } 187 } 188 eggs = eggs.filter((e) => !e.collected); 189 190 /* lava rises periodically â grab low players */ 191 lavaT -= dt; 192 if (lavaT <= 0 && !lava.active) { lava.active = true; lava.t = 3; lavaT = rnd(14, 22); } 193 if (lava.active) { 194 lava.t -= dt; 195 if (lava.t <= 0) lava.active = false; 196 if (!player.dead && player.y > GROUND_Y - 50) killPlayer(); 197 for (const e of enemies) if (e.y > GROUND_Y - 50) { e.dead = true; burst(e.x, e.y, 10, "#ff5722"); } 198 enemies = enemies.filter((e) => !e.dead); 199 } 200 201 /* wave clear */ 202 if (enemies.length === 0 && eggs.length === 0) { 203 wave++; 204 spawnWave(); 205 sound.tone(880, 0.16, { vol: 0.2, slide: 1320 }); 206 } 207 } 208 209 /* ---------------- draw ---------------- */ 210 function drawKnight(e, col, accent) { 211 const x = e.x, y = e.y, d = e.dir; 212 g.save(); 213 g.translate(x, y); 214 g.scale(d, 1); 215 const flapping = e.flapT > 0; 216 /* mount body (ostrich) */ 217 g.fillStyle = col; 218 g.beginPath(); g.ellipse(0, 6, 16, 11, 0, 0, Math.PI * 2); g.fill(); 219 /* neck + head */ 220 g.strokeStyle = col; g.lineWidth = 5; 221 g.beginPath(); g.moveTo(8, 0); g.lineTo(16, -12); g.stroke(); 222 g.fillStyle = col; 223 g.beginPath(); g.arc(17, -14, 5, 0, Math.PI * 2); g.fill(); 224 g.fillStyle = "#ffe600"; 225 g.beginPath(); g.moveTo(21, -14); g.lineTo(27, -12); g.lineTo(21, -10); g.closePath(); g.fill(); 226 /* wing */ 227 g.fillStyle = accent; 228 if (flapping) { g.beginPath(); g.ellipse(-2, -6, 12, 6, -0.6, 0, Math.PI * 2); g.fill(); } 229 else { g.beginPath(); g.ellipse(-2, 4, 11, 5, 0.3, 0, Math.PI * 2); g.fill(); } 230 /* legs */ 231 g.strokeStyle = "#ff9d2e"; g.lineWidth = 2; 232 g.beginPath(); g.moveTo(-4, 14); g.lineTo(-6, 24); g.moveTo(4, 14); g.lineTo(6, 24); g.stroke(); 233 /* rider */ 234 g.fillStyle = accent; 235 g.fillRect(-6, -16, 10, 12); 236 g.fillStyle = "#f2c9a0"; 237 g.beginPath(); g.arc(0, -19, 4, 0, Math.PI * 2); g.fill(); 238 /* lance */ 239 g.strokeStyle = "#fff"; g.lineWidth = 3; 240 g.beginPath(); g.moveTo(2, -12); g.lineTo(26, -16); g.stroke(); 241 g.fillStyle = "#ff4f6d"; 242 g.beginPath(); g.moveTo(26, -16); g.lineTo(32, -18); g.lineTo(26, -12); g.closePath(); g.fill(); 243 g.restore(); 244 } 245 246 function draw() { 247 g.fillStyle = "#0a0616"; g.fillRect(0, 0, LW, LH); 248 /* stars */ 249 g.fillStyle = "rgba(150,120,200,0.3)"; 250 for (let i = 0; i < 40; i++) g.fillRect((i * 137) % LW, (i * 71) % 600, 2, 2); 251 252 /* platforms */ 253 for (const p of PLATS) { 254 g.fillStyle = "#c23b4e"; 255 g.fillRect(p.x, p.y, p.w, 12); 256 g.fillStyle = "#ff7b8f"; 257 g.fillRect(p.x, p.y, p.w, 3); 258 g.fillStyle = "#7d2233"; 259 for (let x = p.x + 8; x < p.x + p.w; x += 20) g.fillRect(x, p.y + 4, 2, 6); 260 } 261 262 /* lava / ground */ 263 if (lava.active) { 264 const flick = Math.sin(Date.now() / 80) * 4; 265 g.fillStyle = "#ff5722"; 266 g.fillRect(0, GROUND_Y - 30 + flick, LW, 60); 267 g.fillStyle = "#ffe600"; 268 for (let x = 0; x < LW; x += 30) g.fillRect(x, GROUND_Y - 30 + flick + Math.sin(x / 20) * 4, 16, 6); 269 /* lava hands */ 270 g.fillStyle = "#ff9d2e"; 271 for (let x = 60; x < LW; x += 160) { 272 g.beginPath(); g.arc(x, GROUND_Y - 24 + flick, 10, Math.PI, 0); g.fill(); 273 g.fillRect(x - 10, GROUND_Y - 24 + flick, 20, 20); 274 } 275 } else { 276 g.fillStyle = "#1a0a2a"; 277 g.fillRect(0, GROUND_Y, LW, LH - GROUND_Y); 278 g.strokeStyle = "#3a2a5a"; g.lineWidth = 2; 279 g.beginPath(); g.moveTo(0, GROUND_Y); g.lineTo(LW, GROUND_Y); g.stroke(); 280 } 281 282 /* eggs */ 283 for (const eg of eggs) { 284 const blink = eg.hatchT < 2 && Math.floor(Date.now() / 150) % 2 === 0; 285 g.fillStyle = blink ? "#ff9d2e" : "#f5f0e0"; 286 g.beginPath(); g.ellipse(eg.x, eg.y, 8, 10, 0, 0, Math.PI * 2); g.fill(); 287 g.fillStyle = "rgba(255,255,255,0.5)"; 288 g.beginPath(); g.ellipse(eg.x - 2, eg.y - 3, 2, 3, 0, 0, Math.PI * 2); g.fill(); 289 } 290 291 /* enemies */ 292 for (const e of enemies) { 293 const col = e.type === "shadow" ? "#c56bff" : e.type === "hunter" ? "#ff4f6d" : "#ff9d2e"; 294 drawKnight(e, col, "#fff"); 295 } 296 297 /* player */ 298 if (!player.dead) drawKnight(player, "#39e0ff", "#ffe600"); 299 300 /* particles */ 301 for (const p of particles) { g.globalAlpha = Math.max(0, p.life * 2); g.fillStyle = p.col; g.fillRect(p.x, p.y, p.size, p.size); } 302 g.globalAlpha = 1; 303 304 /* HUD */ 305 g.font = "bold 16px 'Courier New', monospace"; g.textAlign = "left"; 306 g.fillStyle = "#ff4f6d"; g.fillText("SCORE", 16, 26); 307 g.fillStyle = "#fff"; g.fillText(String(score).padStart(6, "0"), 16, 46); 308 g.textAlign = "right"; 309 g.fillStyle = "#7df9ff"; g.fillText("WAVE " + wave, LW - 16, 26); 310 g.fillStyle = "#39e0ff"; 311 for (let i = 0; i < lives; i++) g.fillRect(LW - 16 - (i + 1) * 18, 36, 12, 8); 312 g.textAlign = "left"; 313 314 if (msgT > 0 && state === "play") { 315 g.fillStyle = "rgba(4,6,13,0.6)"; g.fillRect(0, LH / 2 - 40, LW, 64); 316 g.fillStyle = "#ffe600"; g.font = "bold 30px 'Courier New', monospace"; g.textAlign = "center"; 317 g.fillText(msg, LW / 2, LH / 2); g.textAlign = "left"; 318 } 319 if (state === "over") { 320 g.fillStyle = "rgba(4,6,13,0.8)"; g.fillRect(0, 0, LW, LH); 321 g.textAlign = "center"; 322 g.fillStyle = "#ff3b30"; g.font = "bold 40px 'Courier New', monospace"; 323 g.fillText("GAME OVER", LW / 2, LH / 2 - 16); 324 g.fillStyle = "#fff"; g.font = "bold 18px 'Courier New', monospace"; 325 g.fillText("SCORE " + String(score).padStart(6, "0"), LW / 2, LH / 2 + 20); 326 g.fillStyle = "#7df9ff"; g.font = "bold 14px 'Courier New', monospace"; 327 g.fillText("PRESS ENTER TO CONTINUE", LW / 2, LH / 2 + 52); 328 g.textAlign = "left"; 329 } 330 } 331 332 init(); 333 334 return { 335 update, 336 draw, 337 onPress(btn) { 338 if (btn === "start" && state === "over") init(); 339 if (btn === "fire" && state === "play" && !player.dead) flap(player); 340 }, 341 getStats() {
342 return `SCORE ${score.toLocaleString()} · LIVES ${lives} · WAVE ${wave}`; 343 }, 344 dispose() { sound.stopAll(); }, 345 }; 346 } 347 348 Arcade.register("joust", { 349 title: "JOUST", 350 logicalW: LW, 351 logicalH: LH, 352 hint: "ââ MOVE · SPACE/â FLAP · HIT ENEMIES FROM ABOVE · GRAB EGGS · MIND THE LAVA · ESC MENU", 353 create: makeGame, 354 }); 355})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.