PageSourceSearch

https://awknode.com/arcade/games/joust.js?v=arcade-3d-012

js awknode.com collected 2026-10-07 03:23:19 UTC 13,490 bytes, 355 lines download raw bytes

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.