Back to Home
Gistify Snippet
kyiov
Sep 20ยท1 file(s)
Public

flappybird

game Flappy Bird inline WhatsApp

#github-gist#javascript#game#gist:233fabb05661ee0663da0abe84768427
16
Raw
flappybird.jsjavascript
1055 lines
1import crypto from 'crypto'
2
3const flappyHtml = `<style>
4* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
5body {
6 background: #0a0a0a;
7 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
8 color: #fff;
9 min-height: 100dvh;
10 display: flex;
11 flex-direction: column;
12 align-items: center;
13 justify-content: flex-start;
14 padding: 10px 8px;
15 touch-action: manipulation;
16 cursor: pointer;
17}
18h1 {
19 font-size: 19px;
20 margin: 4px 0 2px;
21 letter-spacing: 2px;
22 font-weight: 800;
23 color: #facc15;
24 text-shadow: 0 0 12px rgba(250, 204, 21, 0.4);
25}
26.hdr-stats {
27 font-size: 12px;
28 opacity: 0.9;
29 margin-bottom: 8px;
30 font-weight: 600;
31 display: flex;
32 align-items: center;
33 gap: 8px;
34}
35.hdr-stats span.score { color: #facc15; font-weight: 800; }
36.hdr-stats span.best { color: #38bdf8; font-weight: 800; }
37.audio-btn {
38 background: rgba(255, 255, 255, 0.1);
39 border: 1px solid rgba(255, 255, 255, 0.2);
40 border-radius: 6px;
41 padding: 1px 6px;
42 font-size: 11px;
43 color: #fff;
44 cursor: pointer;
45}
46.stage {
47 width: 100%;
48 max-width: 280px;
49 margin: 0 auto;
50 display: flex;
51 justify-content: center;
52}
53#c {
54 background: #4ec0ca;
55 border: 2px solid rgba(255, 255, 255, 0.2);
56 border-radius: 14px;
57 display: block;
58 width: 280px;
59 height: 560px;
60 max-width: 100%;
61 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7), 0 0 20px rgba(78, 192, 202, 0.2);
62 cursor: pointer;
63 touch-action: manipulation;
64}
65.foot {
66 font-size: 11px;
67 color: #94a3b8;
68 margin-top: 8px;
69 text-align: center;
70 letter-spacing: 0.3px;
71}
72.foot b { color: #38bdf8; }
73</style>
74
75<h1>๐Ÿฆ FLAPPY BIRD</h1>
76<div class="hdr-stats">
77 <span>Skor: <span id="hdrScore" class="score">0</span></span>
78 <span>ยท</span>
79 <span>Terbaik: <span id="hdrBest" class="best">0</span></span>
80 <button id="audioToggle" class="audio-btn" title="Toggle Sound">๐Ÿ”Š</button>
81</div>
82
83<div class="stage">
84 <canvas id="c" width="280" height="560"></canvas>
85</div>
86
87<div class="foot">
88 ๐Ÿ‘† <b>TAP DI MANA SAJA</b> pada layar untuk terbang
89</div>
90
91<script>
92(function () {
93'use strict';
94
95/* ================= Virtual resolution & canvas setup =================
96 Design space = Kimi K3 original proportions (432 wide), extended in height
97 to 864 so that 280x560 is exactly the same aspect (1:2, like Tetris):
98 SCALE = 280/432 = 560/864 = 0.6481. All engine numbers below stay in the
99 original design space, so the physics/feel is the Kimi K3 one untouched. */
100
101const VW = 432, VH = 864;
102const OUT_W = 280, OUT_H = 560; // CSS pixels of the inline canvas
103const SCALE = OUT_W / VW;
104const GROUND_H = 96;
105const GROUND_Y = VH - GROUND_H;
106
107const canvas = document.getElementById('c');
108const ctx = canvas.getContext('2d');
109
110function resize() {
111 const dpr = Math.min(window.devicePixelRatio || 1, 2);
112 canvas.width = Math.round(OUT_W * dpr);
113 canvas.height = Math.round(OUT_H * dpr);
114 const s = SCALE * dpr;
115 ctx.setTransform(s, 0, 0, s, 0, 0);
116}
117window.addEventListener('resize', resize);
118resize();
119
120/* ================= Tuning (original-feel physics) ================= */
121
122const CFG = {
123 gravityUp: 1500, // px/s^2 while rising โ€” lighter, floatier arc
124 gravityDown: 1850, // px/s^2 while falling
125 flap: -500, // impulse velocity px/s โ€” gentle hop, ~83px apex
126 maxFall: 680, // terminal velocity โ€” bird glides down, never plummets
127 birdX: 128,
128 birdR: 14, // hitbox radius (visual is ~18, forgiving on purpose)
129 baseSpeed: 165, // pipe scroll px/s
130 speedBonus: 55, // max extra speed from score
131 gapBase: 200, // = 22.3% tinggi layar, sama seperti versi awal Mithra (125px dari 560)
132 gapMin: 176,
133 pipeW: 76,
134 capH: 28,
135 capOver: 4,
136 spawnDist: 264,
137 rotUp: -0.34, // rad when rising
138 rotDownMax: 1.2, // rad nose-dive cap
139};
140
141/* ================= Audio (synthesized, no assets) ================= */
142
143const Sound = {
144 ctx: null,
145 ensure() {
146 if (!this.ctx) {
147 const AC = window.AudioContext || window.webkitAudioContext;
148 if (AC) this.ctx = new AC();
149 }
150 if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume();
151 },
152 tone(freq0, freq1, dur, type, vol, delay = 0) {
153 if (!this.ctx) return;
154 const t0 = this.ctx.currentTime + delay;
155 const o = this.ctx.createOscillator();
156 const g = this.ctx.createGain();
157 o.type = type;
158 o.frequency.setValueAtTime(freq0, t0);
159 o.frequency.exponentialRampToValueAtTime(Math.max(freq1, 1), t0 + dur);
160 g.gain.setValueAtTime(vol, t0);
161 g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
162 o.connect(g).connect(this.ctx.destination);
163 o.start(t0); o.stop(t0 + dur + 0.02);
164 },
165 flap() { this.tone(420, 700, 0.09, 'triangle', 0.18); },
166 score() { this.tone(880, 880, 0.07, 'sine', 0.22); this.tone(1318, 1318, 0.12, 'sine', 0.22, 0.07); },
167 hit() { this.tone(220, 60, 0.25, 'sawtooth', 0.3); },
168 thud() { this.tone(140, 50, 0.15, 'square', 0.22); },
169 swoosh(){ this.tone(300, 900, 0.18, 'sine', 0.1); },
170};
171
172/* ================= Game state ================= */
173
174let state = 'ready'; // ready | play | dead | over
175let time = 0;
176let score = 0;
177let best = 0;
178try { best = parseInt(localStorage.getItem('mithra_fb_best') || '0', 10) || 0; } catch (e) { best = 0; }
179let newBest = false;
180
181const bird = { y: VH * 0.44, vy: 0, rot: 0, wingPhase: 0, flapBurst: 0, blink: 0, blinkTimer: 2.2 };
182
183let pipes = []; // {x, gapY, passed}
184let lastGapY = null; // gapY pipa terakhir, buat batasi lompatan vertikal
185let spawnLeft = 0;
186let groundScroll = 0;
187let hillScroll = 0, bushScroll = 0;
188
189let particles = []; // {x,y,vx,vy,life,maxLife,size,type,rot,vr}
190let popups = []; // {x,y,life,text}
191let shake = 0, flash = 0;
192let deadTimer = 0, panelT = 0, overLock = 0;
193let btnRect = { x: VW/2 - 100, y: 0, w: 200, h: 56 }; // y set during panel layout
194let btnPress = false;
195
196/* Ambient parallax layers */
197const cloudsFar = [], cloudsNear = [], motes = [];
198function seedLayers() {
199 cloudsFar.length = 0; cloudsNear.length = 0; motes.length = 0;
200 for (let i = 0; i < 5; i++) cloudsFar.push({ x: Math.random() * VW * 1.6, y: 40 + Math.random() * 200, s: 0.55 + Math.random() * 0.4 });
201 for (let i = 0; i < 4; i++) cloudsNear.push({ x: Math.random() * VW * 1.6, y: 90 + Math.random() * 260, s: 0.9 + Math.random() * 0.5 });
202 for (let i = 0; i < 12; i++) motes.push({ x: Math.random() * VW, y: 60 + Math.random() * (GROUND_Y - 120), r: 1.2 + Math.random() * 1.8, ph: Math.random() * 6.28, sp: 8 + Math.random() * 14 });
203}
204seedLayers();
205
206/* ================= Helpers ================= */
207
208const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
209const lerp = (a, b, t) => a + (b - a) * t;
210const easeOutBack = x => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); };
211const easeOutCubic = x => 1 - Math.pow(1 - x, 3);
212const hash = n => { const s = Math.sin(n * 127.1) * 43758.5453; return s - Math.floor(s); };
213
214function roundRect(x, y, w, h, r) {
215 ctx.beginPath();
216 ctx.moveTo(x + r, y);
217 ctx.arcTo(x + w, y, x + w, y + h, r);
218 ctx.arcTo(x + w, y + h, x, y + h, r);
219 ctx.arcTo(x, y + h, x, y, r);
220 ctx.arcTo(x, y, x + w, y, r);
221 ctx.closePath();
222}
223
224function outlinedText(txt, x, y, size, fill, outline, lw, align = 'center', weight = 800) {
225 ctx.font = \`\${weight} \${size}px system-ui, sans-serif\`;
226 ctx.textAlign = align;
227 ctx.textBaseline = 'middle';
228 ctx.lineJoin = 'round';
229 ctx.lineWidth = lw;
230 ctx.strokeStyle = outline;
231 ctx.strokeText(txt, x, y);
232 ctx.fillStyle = fill;
233 ctx.fillText(txt, x, y);
234}
235
236function circleRect(cx, cy, r, rx, ry, rw, rh) {
237 const nx = clamp(cx, rx, rx + rw);
238 const ny = clamp(cy, ry, ry + rh);
239 const dx = cx - nx, dy = cy - ny;
240 return dx * dx + dy * dy < r * r;
241}
242
243/* ================= Flow ================= */
244
245function reset() {
246 score = 0;
247 newBest = false;
248 pipes = [];
249 lastGapY = null;
250 spawnLeft = CFG.spawnDist * 0.75;
251 bird.y = VH * 0.44;
252 bird.vy = 0;
253 bird.rot = 0;
254 bird.flapBurst = 0;
255 particles = [];
256 popups = [];
257 shake = 0; flash = 0;
258 deadTimer = 0; panelT = 0; overLock = 0;
259 state = 'ready';
260}
261
262function flap() {
263 bird.vy = CFG.flap;
264 bird.flapBurst = 0.22;
265 Sound.flap();
266 for (let i = 0; i < 3; i++) {
267 particles.push({
268 x: CFG.birdX - 12 + Math.random() * 6, y: bird.y + 12,
269 vx: -40 - Math.random() * 40, vy: 40 + Math.random() * 60,
270 life: 0, maxLife: 0.4 + Math.random() * 0.2,
271 size: 3 + Math.random() * 3, type: 'puff', rot: 0, vr: 0,
272 });
273 }
274}
275
276function spawnPipe() {
277 /* Logic versi awal Mithra: sisa pipa atas & bawah selalu minimal 12.5% tinggi
278 layar (dulu 70px dari 560px) -> tidak ada lagi pipa yang mepet langit atau
279 mepet tanah. Lompatan antar pipa juga dibatasi supaya tidak ada zig-zag
280 atas-bawah yang mustahil. */
281 const gap = currentGap();
282 const edge = VH * 0.125;
283 const minY = edge + gap / 2;
284 const maxY = GROUND_Y - edge - gap / 2;
285 let gapY = minY + Math.random() * (maxY - minY);
286 if (lastGapY !== null) {
287 const swing = (maxY - minY) * 0.65;
288 gapY = clamp(gapY, lastGapY - swing, lastGapY + swing);
289 }
290 lastGapY = gapY;
291 pipes.push({ x: VW + 10, gapY, passed: false });
292}
293
294function currentSpeed() { return CFG.baseSpeed + Math.min(score * 2.5, CFG.speedBonus); }
295function currentGap() { return Math.max(CFG.gapBase - score * 0.8, CFG.gapMin); }
296
297function die(onGround) {
298 if (state !== 'play') return;
299 state = 'dead';
300 deadTimer = 0;
301 shake = 0.55;
302 flash = 1;
303 Sound.hit();
304 if (!onGround) bird.vy = Math.min(bird.vy, -380);
305 for (let i = 0; i < 14; i++) {
306 const a = Math.random() * Math.PI * 2;
307 particles.push({
308 x: CFG.birdX, y: bird.y,
309 vx: Math.cos(a) * (60 + Math.random() * 160), vy: Math.sin(a) * (60 + Math.random() * 160) - 120,
310 life: 0, maxLife: 0.8 + Math.random() * 0.5,
311 size: 4 + Math.random() * 4, type: 'feather', rot: Math.random() * 6.28, vr: (Math.random() - 0.5) * 12,
312 });
313 }
314}
315
316function gameOver() {
317 state = 'over';
318 panelT = 0;
319 overLock = 0.7;
320 if (score > best) { best = score; newBest = true; try { localStorage.setItem('mithra_fb_best', String(best)); } catch (e) {} }
321 Sound.swoosh();
322}
323
324function handleAction(x, y) {
325 Sound.ensure();
326 if (state === 'ready') { state = 'play'; flap(); }
327 else if (state === 'play') { flap(); }
328 else if (state === 'over' && overLock <= 0) {
329 if (x === undefined || (x >= btnRect.x && x <= btnRect.x + btnRect.w && y >= btnRect.y && y <= btnRect.y + btnRect.h)) {
330 reset();
331 }
332 }
333}
334
335/* Input */
336function toVirtual(e) {
337 const r = canvas.getBoundingClientRect();
338 return { x: (e.clientX - r.left) / r.width * VW, y: (e.clientY - r.top) / r.height * VH };
339}
340canvas.addEventListener('pointerdown', e => {
341 e.preventDefault();
342 const p = toVirtual(e);
343 btnPress = state === 'over' && p.x >= btnRect.x && p.x <= btnRect.x + btnRect.w && p.y >= btnRect.y && p.y <= btnRect.y + btnRect.h;
344 handleAction(p.x, p.y);
345});
346window.addEventListener('pointerup', () => { btnPress = false; });
347window.addEventListener('keydown', e => {
348 if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'Enter') {
349 e.preventDefault();
350 if (!e.repeat) handleAction();
351 }
352});
353document.addEventListener('visibilitychange', () => { last = performance.now(); });
354
355/* ================= Update ================= */
356
357function update(dt) {
358 time += dt;
359 const speed = state === 'play' || state === 'dead' ? currentSpeed() : 40;
360
361 /* Parallax always breathes */
362 groundScroll = (groundScroll + speed * dt) % 48;
363 hillScroll = (hillScroll + speed * 0.35 * dt) % 1000;
364 bushScroll = (bushScroll + speed * 0.6 * dt) % 1000;
365 for (const c of cloudsFar) { c.x -= speed * 0.12 * dt; if (c.x < -90 * c.s) { c.x = VW + 90 * c.s; c.y = 40 + Math.random() * 200; } }
366 for (const c of cloudsNear) { c.x -= speed * 0.25 * dt; if (c.x < -110 * c.s) { c.x = VW + 110 * c.s; c.y = 90 + Math.random() * 260; } }
367 for (const m of motes) { m.x -= m.sp * dt; m.y += Math.sin(time * 1.5 + m.ph) * 6 * dt; if (m.x < -6) { m.x = VW + 6; m.y = 60 + Math.random() * (GROUND_Y - 120); } }
368
369 /* Particles & popups */
370 for (let i = particles.length - 1; i >= 0; i--) {
371 const p = particles[i];
372 p.life += dt;
373 if (p.type === 'feather') { p.vy += 900 * dt; p.vx *= 0.99; p.rot += p.vr * dt; }
374 if (p.type === 'puff') { p.vy -= 60 * dt; }
375 p.x += p.vx * dt;
376 p.y += p.vy * dt;
377 if (p.life >= p.maxLife) particles.splice(i, 1);
378 }
379 for (let i = popups.length - 1; i >= 0; i--) {
380 popups[i].life += dt;
381 popups[i].y -= 50 * dt;
382 if (popups[i].life > 0.8) popups.splice(i, 1);
383 }
384
385 shake = Math.max(0, shake - dt * 1.6);
386 flash = Math.max(0, flash - dt * 2.4);
387
388 /* Bird */
389 bird.blinkTimer -= dt;
390 if (bird.blinkTimer <= 0) { bird.blink = 0.12; bird.blinkTimer = 2.4 + Math.random() * 2; }
391 bird.blink = Math.max(0, bird.blink - dt);
392 bird.flapBurst = Math.max(0, bird.flapBurst - dt);
393 bird.wingPhase += dt * (bird.flapBurst > 0 ? 34 : (state === 'play' || state === 'dead' ? 10 : 7));
394
395 if (state === 'ready') {
396 bird.y = VH * 0.44 + Math.sin(time * 2.6) * 9;
397 bird.rot = Math.sin(time * 2.6 + 0.6) * 0.08;
398 return;
399 }
400
401 if (state === 'play' || state === 'dead') {
402 const g = bird.vy < 0 ? CFG.gravityUp : CFG.gravityDown;
403 bird.vy = Math.min(bird.vy + g * dt, CFG.maxFall);
404 bird.y += bird.vy * dt;
405 if (bird.y < CFG.birdR + 2) { bird.y = CFG.birdR + 2; bird.vy = Math.max(bird.vy, 0); }
406
407 const target = bird.vy < 0 ? CFG.rotUp : Math.min(CFG.rotDownMax, (bird.vy / CFG.maxFall) * CFG.rotDownMax * 1.6);
408 const rate = bird.vy < 0 ? 18 : (state === 'dead' ? 6 : 4.5);
409 bird.rot += (target - bird.rot) * Math.min(1, rate * dt);
410 }
411
412 if (state === 'play') {
413 /* Pipes */
414 spawnLeft -= currentSpeed() * dt;
415 if (spawnLeft <= 0) { spawnPipe(); spawnLeft += CFG.spawnDist; }
416 for (let i = pipes.length - 1; i >= 0; i--) {
417 const p = pipes[i];
418 p.x -= currentSpeed() * dt;
419 if (!p.passed && p.x + CFG.pipeW < CFG.birdX - CFG.birdR) {
420 p.passed = true;
421 score++;
422 Sound.score();
423 popups.push({ x: CFG.birdX + 30, y: bird.y - 26, life: 0, text: '+1' });
424 }
425 if (p.x + CFG.pipeW < -20) pipes.splice(i, 1);
426 }
427
428 /* Collisions */
429 const gap = currentGap();
430 for (const p of pipes) {
431 const topB = p.gapY - gap / 2;
432 const botT = p.gapY + gap / 2;
433 if (circleRect(CFG.birdX, bird.y, CFG.birdR, p.x, -60, CFG.pipeW, topB + 60) ||
434 circleRect(CFG.birdX, bird.y, CFG.birdR, p.x, botT, CFG.pipeW, GROUND_Y - botT)) {
435 die(false);
436 break;
437 }
438 }
439 if (state === 'play' && bird.y + CFG.birdR >= GROUND_Y) {
440 bird.y = GROUND_Y - CFG.birdR;
441 die(true);
442 Sound.thud();
443 }
444 }
445
446 if (state === 'dead') {
447 deadTimer += dt;
448 for (const p of pipes) p.x -= 0; // world freezes on death
449 if (bird.y + CFG.birdR >= GROUND_Y) {
450 bird.y = GROUND_Y - CFG.birdR;
451 bird.vy = 0;
452 bird.rot += (CFG.rotDownMax - bird.rot) * Math.min(1, 10 * dt);
453 if (deadTimer > 0.55) gameOver();
454 }
455 }
456
457 if (state === 'over') {
458 panelT += dt;
459 overLock = Math.max(0, overLock - dt);
460 }
461}
462
463/* ================= Render ================= */
464
465let skyGrad = null;
466
467function drawSky() {
468 if (!skyGrad) {
469 skyGrad = ctx.createLinearGradient(0, 0, 0, VH);
470 skyGrad.addColorStop(0, '#7EC8F7');
471 skyGrad.addColorStop(0.55, '#A9DDF5');
472 skyGrad.addColorStop(1, '#FDE9C8');
473 }
474 ctx.fillStyle = skyGrad;
475 ctx.fillRect(0, 0, VW, VH);
476
477 /* Sun */
478 const sx = VW - 82, sy = 96;
479 const glow = ctx.createRadialGradient(sx, sy, 8, sx, sy, 70);
480 glow.addColorStop(0, 'rgba(255,240,180,0.9)');
481 glow.addColorStop(1, 'rgba(255,240,180,0)');
482 ctx.fillStyle = glow;
483 ctx.fillRect(sx - 70, sy - 70, 140, 140);
484 ctx.fillStyle = '#FFDF6B';
485 ctx.beginPath(); ctx.arc(sx, sy, 30, 0, 6.29); ctx.fill();
486 ctx.fillStyle = '#F5B93B';
487 ctx.beginPath(); ctx.arc(sx - 8, sy - 2, 2.6, 0, 6.29); ctx.fill();
488 ctx.beginPath(); ctx.arc(sx + 8, sy - 2, 2.6, 0, 6.29); ctx.fill();
489 ctx.strokeStyle = '#F5B93B';
490 ctx.lineWidth = 2.4;
491 ctx.lineCap = 'round';
492 ctx.beginPath(); ctx.arc(sx, sy + 5, 7, 0.25, Math.PI - 0.25); ctx.stroke();
493}
494
495function drawCloud(c, shade) {
496 ctx.save();
497 ctx.translate(c.x, c.y);
498 ctx.scale(c.s, c.s);
499 ctx.fillStyle = shade;
500 ctx.beginPath();
501 ctx.arc(0, 8, 20, 0, 6.29);
502 ctx.arc(24, 8, 15, 0, 6.29);
503 ctx.arc(-24, 10, 14, 0, 6.29);
504 ctx.arc(6, -6, 16, 0, 6.29);
505 ctx.fill();
506 ctx.fillStyle = '#FFFFFF';
507 ctx.beginPath();
508 ctx.arc(0, 4, 20, 0, 6.29);
509 ctx.arc(24, 5, 15, 0, 6.29);
510 ctx.arc(-24, 7, 14, 0, 6.29);
511 ctx.arc(6, -9, 16, 0, 6.29);
512 ctx.fill();
513 ctx.restore();
514}
515
516function drawHillLayer(scroll, baseY, bumpW, color1, color2) {
517 const off = scroll % (bumpW * 2);
518 for (let i = -2; i < VW / bumpW + 3; i++) {
519 const x = i * bumpW - off;
520 const h = 34 + hash(i + Math.floor(scroll / (bumpW * 2)) * 7) * 40;
521 ctx.fillStyle = i % 2 === 0 ? color1 : color2;
522 ctx.beginPath();
523 ctx.ellipse(x + bumpW / 2, baseY, bumpW * 0.62, h, 0, 0, 6.29);
524 ctx.fill();
525 }
526}
527
528function drawBackground() {
529 drawSky();
530 for (const c of cloudsFar) drawCloud(c, 'rgba(210,230,245,0.6)');
531 drawHillLayer(hillScroll, GROUND_Y - 40, 150, '#BFE3C0', '#AAD9B4');
532 for (const c of cloudsNear) drawCloud(c, 'rgba(220,238,250,0.8)');
533 drawHillLayer(bushScroll, GROUND_Y - 8, 84, '#8ED16B', '#7CC75A');
534
535 /* Motes */
536 ctx.fillStyle = 'rgba(255,255,255,0.75)';
537 for (const m of motes) {
538 ctx.globalAlpha = 0.35 + Math.sin(time * 1.5 + m.ph) * 0.25;
539 ctx.beginPath(); ctx.arc(m.x, m.y, m.r, 0, 6.29); ctx.fill();
540 }
541 ctx.globalAlpha = 1;
542}
543
544function drawPipeBody(x, y, h, isTop) {
545 const w = CFG.pipeW;
546 const g = ctx.createLinearGradient(x, 0, x + w, 0);
547 g.addColorStop(0, '#9BDB7C');
548 g.addColorStop(0.18, '#7CC75A');
549 g.addColorStop(0.8, '#5FAE3E');
550 g.addColorStop(1, '#4E9633');
551 ctx.fillStyle = g;
552 ctx.fillRect(x, y, w, h);
553 ctx.fillStyle = 'rgba(255,255,255,0.28)';
554 ctx.fillRect(x + 7, y, 7, h);
555 ctx.fillStyle = 'rgba(0,0,0,0.12)';
556 ctx.fillRect(x + w - 9, y, 9, h);
557
558 /* Cap */
559 const capY = isTop ? y + h - CFG.capH : y;
560 const cg = ctx.createLinearGradient(x - CFG.capOver, 0, x + w + CFG.capOver, 0);
561 cg.addColorStop(0, '#A5E187');
562 cg.addColorStop(0.5, '#74C455');
563 cg.addColorStop(1, '#4E9633');
564 ctx.fillStyle = cg;
565 roundRect(x - CFG.capOver, capY, w + CFG.capOver * 2, CFG.capH, 7);
566 ctx.fill();
567 ctx.strokeStyle = 'rgba(46,110,30,0.55)';
568 ctx.lineWidth = 2.5;
569 ctx.stroke();
570 ctx.fillStyle = 'rgba(255,255,255,0.35)';
571 roundRect(x - CFG.capOver + 5, capY + 4, w + CFG.capOver * 2 - 10, 5, 2.5);
572 ctx.fill();
573 ctx.strokeStyle = 'rgba(46,110,30,0.55)';
574 ctx.lineWidth = 2.5;
575 ctx.strokeRect(x + 1.2, y + (isTop ? -2 : 2), w - 2.4, h - 2);
576}
577
578function drawPipes() {
579 const gap = currentGap();
580 for (const p of pipes) {
581 const topH = p.gapY - gap / 2;
582 const botY = p.gapY + gap / 2;
583 drawPipeBody(p.x, -4, topH + 4, true);
584 drawPipeBody(p.x, botY, GROUND_Y - botY + 4, false);
585 }
586}
587
588function drawGround() {
589 /* Dirt */
590 ctx.fillStyle = '#E9CD97';
591 ctx.fillRect(0, GROUND_Y + 22, VW, GROUND_H - 22);
592 ctx.save();
593 ctx.beginPath();
594 ctx.rect(0, GROUND_Y + 22, VW, GROUND_H - 22);
595 ctx.clip();
596 ctx.fillStyle = 'rgba(180,140,80,0.25)';
597 for (let x = -48; x < VW + 48; x += 48) {
598 const sx = x - groundScroll;
599 ctx.beginPath();
600 ctx.moveTo(sx, GROUND_Y + 22);
601 ctx.lineTo(sx + 22, GROUND_Y + 22);
602 ctx.lineTo(sx + 6, VH);
603 ctx.lineTo(sx - 16, VH);
604 ctx.closePath();
605 ctx.fill();
606 }
607 ctx.restore();
608 ctx.fillStyle = 'rgba(120,85,40,0.35)';
609 ctx.fillRect(0, VH - 8, VW, 8);
610
611 /* Grass */
612 const g = ctx.createLinearGradient(0, GROUND_Y, 0, GROUND_Y + 24);
613 g.addColorStop(0, '#95D870');
614 g.addColorStop(1, '#6DBE4B');
615 ctx.fillStyle = g;
616 ctx.fillRect(0, GROUND_Y, VW, 24);
617 ctx.fillStyle = '#B7EC96';
618 ctx.fillRect(0, GROUND_Y, VW, 5);
619 /* Scalloped grass edge */
620 ctx.fillStyle = '#6DBE4B';
621 for (let x = -24; x < VW + 24; x += 24) {
622 ctx.beginPath();
623 ctx.arc(x - (groundScroll % 24) + 12, GROUND_Y + 24, 8, 0, Math.PI);
624 ctx.fill();
625 }
626 ctx.fillStyle = 'rgba(46,110,30,0.4)';
627 ctx.fillRect(0, GROUND_Y + 21, VW, 3);
628}
629
630function drawBird() {
631 ctx.save();
632 ctx.translate(CFG.birdX, bird.y);
633 ctx.rotate(bird.rot);
634
635 /* Tail */
636 ctx.fillStyle = '#F0922B';
637 ctx.beginPath();
638 ctx.ellipse(-17, 3, 8, 4.5, -0.5, 0, 6.29);
639 ctx.fill();
640 ctx.beginPath();
641 ctx.ellipse(-16, 7, 7, 4, -0.15, 0, 6.29);
642 ctx.fill();
643
644 /* Body */
645 const bg = ctx.createRadialGradient(2, -6, 3, 0, 0, 24);
646 bg.addColorStop(0, '#FFE066');
647 bg.addColorStop(1, '#FFB830');
648 ctx.fillStyle = bg;
649 ctx.beginPath();
650 ctx.ellipse(0, 0, 19, 16, 0, 0, 6.29);
651 ctx.fill();
652 ctx.strokeStyle = 'rgba(200,120,20,0.5)';
653 ctx.lineWidth = 2;
654 ctx.stroke();
655
656 /* Belly */
657 ctx.fillStyle = '#FFF1B8';
658 ctx.beginPath();
659 ctx.ellipse(3, 7, 11, 7.5, 0, 0, 6.29);
660 ctx.fill();
661
662 /* Wing (flapping) */
663 const wingAng = Math.sin(bird.wingPhase) * (bird.flapBurst > 0 ? 0.9 : 0.45) - 0.15;
664 ctx.save();
665 ctx.translate(-3, 1);
666 ctx.rotate(wingAng);
667 const wg = ctx.createLinearGradient(-14, -8, 4, 8);
668 wg.addColorStop(0, '#FFC94D');
669 wg.addColorStop(1, '#F0922B');
670 ctx.fillStyle = wg;
671 ctx.beginPath();
672 ctx.ellipse(-7, 0, 11, 7, -0.35, 0, 6.29);
673 ctx.fill();
674 ctx.strokeStyle = 'rgba(190,110,20,0.55)';
675 ctx.lineWidth = 2;
676 ctx.stroke();
677 ctx.restore();
678
679 /* Beak (opens on flap) */
680 const open = bird.flapBurst > 0 ? 3.2 : (bird.vy < -120 ? 2.2 : 0.8);
681 ctx.fillStyle = '#FF7F3F';
682 ctx.beginPath();
683 ctx.moveTo(13, -3.5);
684 ctx.lineTo(25, -1.5 - open * 0.4);
685 ctx.lineTo(13.5, 1.5);
686 ctx.closePath();
687 ctx.fill();
688 ctx.fillStyle = '#F2662D';
689 ctx.beginPath();
690 ctx.moveTo(13.5, 1.2);
691 ctx.lineTo(23, 1 + open);
692 ctx.lineTo(13, 4.5);
693 ctx.closePath();
694 ctx.fill();
695
696 /* Eye */
697 if (bird.blink > 0) {
698 ctx.strokeStyle = '#5A3A1A';
699 ctx.lineWidth = 2.4;
700 ctx.lineCap = 'round';
701 ctx.beginPath();
702 ctx.arc(8, -5.5, 4.5, 0.15, Math.PI - 0.15);
703 ctx.stroke();
704 } else {
705 ctx.fillStyle = '#FFFFFF';
706 ctx.beginPath(); ctx.arc(8, -6, 6.5, 0, 6.29); ctx.fill();
707 ctx.strokeStyle = 'rgba(190,110,20,0.4)';
708 ctx.lineWidth = 1.6;
709 ctx.stroke();
710 ctx.fillStyle = '#3A2A18';
711 ctx.beginPath(); ctx.arc(9.8, -6, 3.1, 0, 6.29); ctx.fill();
712 ctx.fillStyle = '#FFFFFF';
713 ctx.beginPath(); ctx.arc(11, -7.4, 1.3, 0, 6.29); ctx.fill();
714 }
715
716 /* Blush */
717 ctx.fillStyle = 'rgba(255,120,120,0.45)';
718 ctx.beginPath(); ctx.arc(5, 1.5, 3.4, 0, 6.29); ctx.fill();
719
720 ctx.restore();
721}
722
723function drawParticles() {
724 for (const p of particles) {
725 const t = 1 - p.life / p.maxLife;
726 if (p.type === 'puff') {
727 ctx.globalAlpha = t * 0.55;
728 ctx.fillStyle = '#FFFFFF';
729 ctx.beginPath(); ctx.arc(p.x, p.y, p.size * (1 + p.life * 2.4), 0, 6.29); ctx.fill();
730 } else {
731 ctx.save();
732 ctx.translate(p.x, p.y);
733 ctx.rotate(p.rot);
734 ctx.globalAlpha = Math.min(1, t * 1.6);
735 ctx.fillStyle = '#FFC94D';
736 ctx.beginPath(); ctx.ellipse(0, 0, p.size, p.size * 0.5, 0, 0, 6.29); ctx.fill();
737 ctx.restore();
738 }
739 }
740 ctx.globalAlpha = 1;
741}
742
743function drawPopups() {
744 for (const p of popups) {
745 const t = 1 - p.life / 0.8;
746 ctx.globalAlpha = t;
747 outlinedText(p.text, p.x, p.y, 26, '#FFFFFF', 'rgba(90,60,20,0.85)', 5);
748 }
749 ctx.globalAlpha = 1;
750}
751
752function drawScore() {
753 outlinedText(String(score), VW / 2, 78, 52, '#FFFFFF', 'rgba(60,40,20,0.9)', 9);
754}
755
756function drawReady() {
757 /* Wavy title, per-letter bounce */
758 const title = 'FLUPPY BIRD';
759 const size = 46;
760 ctx.font = \`800 \${size}px system-ui, sans-serif\`;
761 const widths = [...title].map(ch => ctx.measureText(ch).width);
762 const total = widths.reduce((a, b) => a + b, 0);
763 let x = VW / 2 - total / 2;
764 for (let i = 0; i < title.length; i++) {
765 const ch = title[i];
766 const wob = Math.sin(time * 2.6 + i * 0.55) * 6;
767 const rot = Math.sin(time * 2.6 + i * 0.55 + 0.7) * 0.06;
768 ctx.save();
769 ctx.translate(x + widths[i] / 2, 170 + wob);
770 ctx.rotate(rot);
771 const fill = i < 6 ? '#FF8A5C' : '#4FB6E8';
772 outlinedText(ch, 0, 0, size, fill, 'rgba(255,255,255,0.95)', 10);
773 ctx.restore();
774 x += widths[i];
775 }
776
777 outlinedText('siap terbang?', VW / 2, 226, 24, '#FFFFFF', 'rgba(60,40,20,0.85)', 6);
778
779 /* Pulsing hint chip */
780 const pulse = 0.75 + Math.sin(time * 4) * 0.25;
781 ctx.globalAlpha = pulse;
782 /* Chip lebar-ikut-teks: teks hint diukur dulu, kalau kepanjangan fontnya
783 dikecilin dan chip dibatasi maxChipW supaya tidak keluar dari border kanvas. */
784 const hint = 'ketuk / spasi untuk terbang';
785 const chipH = 52;
786 const padX = 26;
787 const maxChipW = VW - 56;
788 let hintSize = 21;
789 ctx.font = \`800 \${hintSize}px system-ui, sans-serif\`;
790 let hintW = ctx.measureText(hint).width;
791 if (hintW + padX * 2 > maxChipW) {
792 hintSize = Math.max(14, Math.floor(hintSize * (maxChipW - padX * 2) / hintW));
793 ctx.font = \`800 \${hintSize}px system-ui, sans-serif\`;
794 hintW = ctx.measureText(hint).width;
795 }
796 const chipW = Math.min(hintW + padX * 2, maxChipW);
797 ctx.fillStyle = 'rgba(255,255,255,0.92)';
798 roundRect(VW / 2 - chipW / 2, VH * 0.62, chipW, chipH, 26);
799 ctx.fill();
800 ctx.strokeStyle = 'rgba(90,60,30,0.35)';
801 ctx.lineWidth = 3;
802 ctx.stroke();
803 ctx.globalAlpha = 1;
804 outlinedText(hint, VW / 2, VH * 0.62 + chipH / 2 + 1, hintSize, '#8A5A2B', 'rgba(255,255,255,0)', 0);
805
806 if (best > 0) {
807 outlinedText(\`terbaik \${best}\`, VW / 2, VH * 0.62 + chipH + 30, 20, '#FFFFFF', 'rgba(60,40,20,0.8)', 5);
808 }
809}
810
811function medalFor(s) {
812 if (s >= 40) return { name: 'platinum', c1: '#DFF8FF', c2: '#9ADCF0', edge: '#6BB8D4' };
813 if (s >= 30) return { name: 'gold', c1: '#FFE37A', c2: '#F5B93B', edge: '#D19224' };
814 if (s >= 20) return { name: 'silver', c1: '#F2F6FA', c2: '#B9C6D2', edge: '#8FA3B5' };
815 if (s >= 10) return { name: 'bronze', c1: '#F2B78A', c2: '#CE7F4A', edge: '#A55E33' };
816 return null;
817}
818
819function drawMedal(x, y, m) {
820 /* Ribbon */
821 ctx.fillStyle = '#E85C6A';
822 ctx.beginPath();
823 ctx.moveTo(x - 10, y - 6);
824 ctx.lineTo(x - 18, y + 24);
825 ctx.lineTo(x - 8, y + 19);
826 ctx.lineTo(x - 2, y + 26);
827 ctx.lineTo(x + 4, y - 4);
828 ctx.closePath();
829 ctx.fill();
830 const g = ctx.createRadialGradient(x - 6, y - 8, 3, x, y, 26);
831 g.addColorStop(0, m.c1);
832 g.addColorStop(1, m.c2);
833 ctx.fillStyle = g;
834 ctx.beginPath(); ctx.arc(x, y, 23, 0, 6.29); ctx.fill();
835 ctx.strokeStyle = m.edge;
836 ctx.lineWidth = 3;
837 ctx.stroke();
838 /* Star */
839 ctx.fillStyle = 'rgba(255,255,255,0.9)';
840 ctx.save();
841 ctx.translate(x, y);
842 ctx.beginPath();
843 for (let i = 0; i < 5; i++) {
844 const a = -Math.PI / 2 + i * (Math.PI * 2 / 5);
845 const a2 = a + Math.PI / 5;
846 ctx.lineTo(Math.cos(a) * 10, Math.sin(a) * 10);
847 ctx.lineTo(Math.cos(a2) * 4.5, Math.sin(a2) * 4.5);
848 }
849 ctx.closePath();
850 ctx.fill();
851 ctx.restore();
852}
853
854function drawOver() {
855 const p = easeOutBack(Math.min(panelT / 0.55, 1));
856 const targetY = VH * 0.42;
857 const py = lerp(-300, targetY, p);
858 const pw = 330, ph = 250;
859 const px = VW / 2 - pw / 2;
860
861 /* Dim */
862 ctx.fillStyle = \`rgba(30,40,60,\${0.25 * Math.min(panelT / 0.4, 1)})\`;
863 ctx.fillRect(0, 0, VW, VH);
864
865 /* Panel */
866 ctx.fillStyle = '#FFF6E5';
867 roundRect(px, py - ph / 2, pw, ph, 22);
868 ctx.fill();
869 ctx.strokeStyle = '#C98F5F';
870 ctx.lineWidth = 4;
871 ctx.stroke();
872 ctx.fillStyle = 'rgba(201,143,95,0.15)';
873 roundRect(px + 8, py - ph / 2 + 8, pw - 16, ph - 16, 15);
874 ctx.fill();
875
876 outlinedText('GAME OVER', VW / 2, py - ph / 2 + 40, 34, '#FF8A5C', 'rgba(120,60,20,0.85)', 7);
877
878 /* Medal */
879 const m = medalFor(score);
880 if (m) {
881 drawMedal(px + 78, py + 12, m);
882 outlinedText(m.name, px + 78, py + 62, 17, '#8A6A45', 'rgba(255,255,255,0)', 0);
883 } else {
884 outlinedText('belum ada medali', px + 78, py + 16, 18, '#B39B80', 'rgba(255,255,255,0)', 0);
885 outlinedText('skor 10+', px + 78, py + 42, 15, '#C4AE95', 'rgba(255,255,255,0)', 0);
886 }
887
888 /* Scores */
889 outlinedText('score', px + 225, py - 16, 18, '#B39B80', 'rgba(255,255,255,0)', 0, 'center', 700);
890 outlinedText(String(score), px + 225, py + 14, 40, '#5A3A1A', 'rgba(255,255,255,0)', 0);
891 outlinedText('best', px + 225, py + 48, 18, '#B39B80', 'rgba(255,255,255,0)', 0, 'center', 700);
892 outlinedText(String(best), px + 225, py + 74, 26, '#8A5A2B', 'rgba(255,255,255,0)', 0);
893
894 if (newBest) {
895 const bw = 64, bh = 26;
896 ctx.save();
897 ctx.translate(px + 268, py - 40);
898 ctx.rotate(0.12 + Math.sin(time * 6) * 0.05);
899 ctx.fillStyle = '#E85C6A';
900 roundRect(-bw / 2, -bh / 2, bw, bh, 13);
901 ctx.fill();
902 outlinedText('BARU!', 0, 1, 17, '#FFFFFF', 'rgba(140,30,40,0.6)', 4);
903 ctx.restore();
904 }
905
906 /* Button */
907 const bw2 = 200, bh2 = 54;
908 btnRect = { x: VW / 2 - bw2 / 2, y: py + ph / 2 - 26, w: bw2, h: bh2 };
909 const press = btnPress ? 3 : 0;
910 ctx.fillStyle = 'rgba(46,110,30,0.35)';
911 roundRect(btnRect.x, btnRect.y + 5, bw2, bh2, 27);
912 ctx.fill();
913 const bg2 = ctx.createLinearGradient(0, btnRect.y + press, 0, btnRect.y + press + bh2);
914 bg2.addColorStop(0, '#8FD96B');
915 bg2.addColorStop(1, '#5FAE3E');
916 ctx.fillStyle = bg2;
917 roundRect(btnRect.x, btnRect.y + press, bw2, bh2, 27);
918 ctx.fill();
919 ctx.strokeStyle = '#3E7A28';
920 ctx.lineWidth = 3;
921 ctx.stroke();
922 outlinedText('main lagi', VW / 2, btnRect.y + press + bh2 / 2 + 1, 24, '#FFFFFF', 'rgba(50,100,30,0.7)', 5);
923}
924
925function render() {
926 ctx.save();
927 if (shake > 0) {
928 const m = shake * 9;
929 ctx.translate((Math.random() - 0.5) * m, (Math.random() - 0.5) * m);
930 }
931
932 drawBackground();
933 drawPipes();
934 drawGround();
935 drawParticles();
936 drawBird();
937 drawPopups();
938
939 if (state === 'play' || state === 'dead') drawScore();
940 if (state === 'ready') drawReady();
941 if (state === 'over') { drawScore(); drawOver(); }
942
943 ctx.restore();
944
945 if (flash > 0) {
946 ctx.fillStyle = \`rgba(255,255,255,\${flash * 0.85})\`;
947 ctx.fillRect(0, 0, VW, VH);
948 }
949}
950
951/* ================= Main loop ================= */
952
953let last = performance.now();
954function frame(now) {
955 const dt = Math.min((now - last) / 1000, 1 / 30);
956 last = now;
957 update(dt);
958 render();
959 syncHdr();
960 requestAnimationFrame(frame);
961}
962requestAnimationFrame(frame);
963// ---- Mithra: header + audio toggle bridge ----
964 const hdrScoreEl = document.getElementById('hdrScore');
965 const hdrBestEl = document.getElementById('hdrBest');
966 const audioToggleEl = document.getElementById('audioToggle');
967 let muted = false;
968 let _hdrS = -1, _hdrB = -1;
969 function syncHdr() {
970 try {
971 if (score !== _hdrS) { _hdrS = score; hdrScoreEl.textContent = String(score); }
972 if (best !== _hdrB) { _hdrB = best; hdrBestEl.textContent = String(best); }
973 } catch (e) {}
974 }
975 audioToggleEl.addEventListener('pointerdown', function(e) {
976 e.stopPropagation();
977 });
978 audioToggleEl.addEventListener('click', function(e) {
979 e.stopPropagation();
980 Sound.ensure();
981 muted = !muted;
982 audioToggleEl.textContent = muted ? '\\uD83D\\uDD07' : '\\uD83D\\uDD0A';
983 });
984
985 // Mute di level Sound: bungkus tone()
986 const _origTone = Sound.tone.bind(Sound);
987 Sound.tone = function(freq0, freq1, dur, type, vol, delay) {
988 if (muted) return;
989 return _origTone(freq0, freq1, dur, type, vol, delay);
990 };
991
992 // Init header dgn best tersimpan
993 try { hdrBestEl.textContent = String(best); } catch (e) {}
994})();
995</script>`
996
997export default function flappyBird(ev) {
998 ev.on({
999 name: 'flappybird',
1000 cmd: ['flappybird', 'flappy'],
1001 tags: 'Game',
1002 desc: 'Mainkan Flappy Bird inline di chat WhatsApp (tap layar untuk terbang)',
1003 run: async (xp, m) => {
1004 await xp.sendMessage(m.chat, { react: { text: '๐Ÿฆ', key: m.key } })
1005
1006 const payloadData = {
1007 response_id: crypto.randomUUID(),
1008 sections: [
1009 {
1010 view_model: {
1011 primitive: {
1012 __typename: 'GenAIaeacdsnwHtmlPrimitive',
1013 payload: flappyHtml,
1014 trusted_sources: []
1015 },
1016 __typename: 'GenAISingleLayoutViewModel'
1017 }
1018 }
1019 ]
1020 }
1021
1022 await xp.relayMessage(
1023 m.chat,
1024 {
1025 messageContextInfo: {
1026 deviceListMetadata: {},
1027 deviceListMetadataVersion: 2,
1028 botMetadata: {}
1029 },
1030 botForwardedMessage: {
1031 message: {
1032 richResponseMessage: {
1033 messageType: 1,
1034 submessages: [{ messageType: 2, messageText: '๐Ÿฆ FLAPPY BIRD ARCADE' }],
1035 unifiedResponse: {
1036 data: Buffer.from(JSON.stringify(payloadData)).toString('base64')
1037 },
1038 contextInfo: {
1039 forwardingScore: 1,
1040 isForwarded: true,
1041 forwardedAiBotMessageInfo: {
1042 botJid: '867051314767696@bot'
1043 },
1044 forwardOrigin: 4
1045 }
1046 }
1047 }
1048 }
1049 },
1050 {}
1051 )
1052 }
1053 })
1054}
1055
Discussion & Comments0

No comments yet โ€” be the first to leave feedback or start a code discussion.

Sign in with your GitHub account to write comments and join the discussion.