Back to Home
Gistify Snippet
Public
flappybird
game Flappy Bird inline WhatsApp
#github-gist#javascript#game#gist:233fabb05661ee0663da0abe84768427
16
Rawflappybird.jsjavascript
1055 lines| 1 | import crypto from 'crypto' |
| 2 | |
| 3 | const 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; } |
| 5 | body { |
| 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 | } |
| 18 | h1 { |
| 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 | |
| 101 | const VW = 432, VH = 864; |
| 102 | const OUT_W = 280, OUT_H = 560; // CSS pixels of the inline canvas |
| 103 | const SCALE = OUT_W / VW; |
| 104 | const GROUND_H = 96; |
| 105 | const GROUND_Y = VH - GROUND_H; |
| 106 | |
| 107 | const canvas = document.getElementById('c'); |
| 108 | const ctx = canvas.getContext('2d'); |
| 109 | |
| 110 | function 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 | } |
| 117 | window.addEventListener('resize', resize); |
| 118 | resize(); |
| 119 | |
| 120 | /* ================= Tuning (original-feel physics) ================= */ |
| 121 | |
| 122 | const 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 | |
| 143 | const 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 | |
| 174 | let state = 'ready'; // ready | play | dead | over |
| 175 | let time = 0; |
| 176 | let score = 0; |
| 177 | let best = 0; |
| 178 | try { best = parseInt(localStorage.getItem('mithra_fb_best') || '0', 10) || 0; } catch (e) { best = 0; } |
| 179 | let newBest = false; |
| 180 | |
| 181 | const bird = { y: VH * 0.44, vy: 0, rot: 0, wingPhase: 0, flapBurst: 0, blink: 0, blinkTimer: 2.2 }; |
| 182 | |
| 183 | let pipes = []; // {x, gapY, passed} |
| 184 | let lastGapY = null; // gapY pipa terakhir, buat batasi lompatan vertikal |
| 185 | let spawnLeft = 0; |
| 186 | let groundScroll = 0; |
| 187 | let hillScroll = 0, bushScroll = 0; |
| 188 | |
| 189 | let particles = []; // {x,y,vx,vy,life,maxLife,size,type,rot,vr} |
| 190 | let popups = []; // {x,y,life,text} |
| 191 | let shake = 0, flash = 0; |
| 192 | let deadTimer = 0, panelT = 0, overLock = 0; |
| 193 | let btnRect = { x: VW/2 - 100, y: 0, w: 200, h: 56 }; // y set during panel layout |
| 194 | let btnPress = false; |
| 195 | |
| 196 | /* Ambient parallax layers */ |
| 197 | const cloudsFar = [], cloudsNear = [], motes = []; |
| 198 | function 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 | } |
| 204 | seedLayers(); |
| 205 | |
| 206 | /* ================= Helpers ================= */ |
| 207 | |
| 208 | const clamp = (v, a, b) => Math.max(a, Math.min(b, v)); |
| 209 | const lerp = (a, b, t) => a + (b - a) * t; |
| 210 | const easeOutBack = x => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); }; |
| 211 | const easeOutCubic = x => 1 - Math.pow(1 - x, 3); |
| 212 | const hash = n => { const s = Math.sin(n * 127.1) * 43758.5453; return s - Math.floor(s); }; |
| 213 | |
| 214 | function 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 | |
| 224 | function 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 | |
| 236 | function 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 | |
| 245 | function 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 | |
| 262 | function 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 | |
| 276 | function 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 | |
| 294 | function currentSpeed() { return CFG.baseSpeed + Math.min(score * 2.5, CFG.speedBonus); } |
| 295 | function currentGap() { return Math.max(CFG.gapBase - score * 0.8, CFG.gapMin); } |
| 296 | |
| 297 | function 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 | |
| 316 | function 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 | |
| 324 | function 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 */ |
| 336 | function 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 | } |
| 340 | canvas.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 | }); |
| 346 | window.addEventListener('pointerup', () => { btnPress = false; }); |
| 347 | window.addEventListener('keydown', e => { |
| 348 | if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'Enter') { |
| 349 | e.preventDefault(); |
| 350 | if (!e.repeat) handleAction(); |
| 351 | } |
| 352 | }); |
| 353 | document.addEventListener('visibilitychange', () => { last = performance.now(); }); |
| 354 | |
| 355 | /* ================= Update ================= */ |
| 356 | |
| 357 | function 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 | |
| 465 | let skyGrad = null; |
| 466 | |
| 467 | function 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 | |
| 495 | function 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 | |
| 516 | function 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 | |
| 528 | function 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 | |
| 544 | function 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 | |
| 578 | function 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 | |
| 588 | function 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 | |
| 630 | function 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 | |
| 723 | function 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 | |
| 743 | function 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 | |
| 752 | function drawScore() { |
| 753 | outlinedText(String(score), VW / 2, 78, 52, '#FFFFFF', 'rgba(60,40,20,0.9)', 9); |
| 754 | } |
| 755 | |
| 756 | function 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 | |
| 811 | function 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 | |
| 819 | function 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 | |
| 854 | function 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 | |
| 925 | function 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 | |
| 953 | let last = performance.now(); |
| 954 | function 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 | } |
| 962 | requestAnimationFrame(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 | |
| 997 | export 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.