1import { createWorld, flap, updateWorld, PHYSICS, scrollSpeed } from './world.js'; 2import { readBest, submitBest } from './score.js'; 3import { attachInput, createDisplay, startLoop } from './runtime.js'; 4import { createArt } from './art.js'; 5import { 6 isMuted, 7 playCrash, 8 playFlap, 9 playLand, 10 playScore, 11 resume as resumeAudio, 12 toggleMuted, 13} from './audio.js'; 14import { 15 assertGameMountOptions, 16 createGameShell, 17 createScoreFlow, 18} from '../../shared/game-integration.js?v=20260911-debug'; 19 20const GAME = Object.freeze({ 21 gameId: 1, 22 slug: 'flappy-parrot', 23 title: 'FLAPPY PARROT', 24 hint: 'click or space to flap · M mute · R restart', 25}); 26const RESTART_DELAY = 0.7; 27const FLAP_POSE = 0.26; 28 29export function mountFlappyParrot(options) { 30 assertGameMountOptions(options, GAME); 31 32 const { container, auth, onExit } = options; 33 const elements = createGameShell(container, GAME); 34 const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false; 35 const display = createDisplay(elements.stage, { 36 width: PHYSICS.viewW, 37 height: PHYSICS.viewH, 38 background: '#080a10', 39 backgroundGradient: ['#1a1040', '#05060f', '#2a0a2e'], 40 }); 41 const input = attachInput(elements.stage); 42 const art = createArt(); 43 const ctx = display.ctx; 44 45 let world = createWorld({ seed: (Date.now() / 1000) | 0 }); 46 let time = 0; 47 let scrollX = 0; 48 let flapTimer = 0; 49 let shake = 0; 50 let newBest = false; 51 let best = readBest(); 52 let runStartedAt = Date.now(); 53 let destroyed = false; 54 let scoreFlow; 55 56 function syncMuteButton() { 57 const muted = isMuted(); 58 elements.soundButton.setAttribute('aria-pressed', String(!muted)); 59 const label = elements.soundButton.querySelector('.toggle-text'); 60 if (label) label.textContent = muted ? 'Sound off' : 'Sound on'; 61 } 62 63 function reset() { 64 if (destroyed) return; 65 world = createWorld({ seed: (Date.now() / 1000) | 0 }); 66 art.clearParticles(); 67 flapTimer = 0; 68 shake = 0; 69 newBest = false; 70 runStartedAt = Date.now(); 71 elements.stage.focus(); 72 } 73 74 scoreFlow = createScoreFlow({ 75 gameId: GAME.gameId, 76 auth, 77 elements, 78 onExit, 79 onPlayAgain: reset, 80 }); 81 82 function tryFlap() { 83 if (destroyed || scoreFlow.isBlocking()) return; 84 resumeAudio(); 85 if (world.phase === 'dead') { 86 if (world.t >= RESTART_DELAY) reset(); 87 return; 88 } 89 flap(world); 90 } 91 92 function onKey(event) { 93 if (event.key === 'm' || event.key === 'M') { 94 toggleMuted(); 95 syncMuteButton(); 96 resumeAudio(); 97 return; 98 } 99 if (event.key === 'r' || event.key === 'R') { 100 if (!scoreFlow.isBlocking()) reset(); 101 return; 102 } 103 if (event.key === ' ' || event.key === 'ArrowUp' || event.key === 'w' || event.key === 'W') { 104 event.preventDefault(); 105 if (!event.repeat) tryFlap(); 106 } 107 } 108 109 function onSoundClick() { 110 resumeAudio(); 111 toggleMuted(); 112 syncMuteButton(); 113 } 114 115 elements.soundButton.addEventListener('click', onSoundClick); 116 document.addEventListener('keydown', onKey); 117 syncMuteButton(); 118 119 const loop = startLoop({ 120 update(dt) { 121 time += dt; 122 if (scoreFlow.isBlocking()) { 123 input.takeClicks(); 124 return; 125 } 126 for (let i = 0; i < input.takeClicks().length; i += 1) tryFlap(); 127 updateWorld(world, dt, { width: PHYSICS.viewW, height: PHYSICS.viewH }); 128 for (const event of world.events) { 129 if (event.type === 'flap') { 130 flapTimer = FLAP_POSE; 131 playFlap(); 132 } else if (event.type === 'score') { 133 playScore(); 134 art.spawnFloat(PHYSICS.birdX + PHYSICS.birdW / 2, world.y, `+${event.gained}`, '#ffe680'); 135 if (submitBest(undefined, event.score)) { 136 newBest = true; 137 best = event.score; 138 } 139 } else if (event.type === 'crash') { 140 playCrash(); 141 art.burst(event.x, event.y); 142 if (!reducedMotion) shake = Math.max(shake, 3.4); 143 } else if (event.type === 'land') { 144 playLand(); 145 art.splat(event.x, event.y); 146 if (!reducedMotion) shake = Math.max(shake, 2); 147 scoreFlow.completeRun(world.score, runStartedAt); 148 } 149 } 150 world.events.length = 0; 151 if (world.phase === 'ready' || world.phase === 'playing') { 152 scrollX += scrollSpeed(world.clears) * dt; 153 } 154 art.updateParticles(dt); 155 flapTimer = Math.max(0, flapTimer - dt); 156 shake *= Math.max(0, 1 - 9 * dt); 157 if (shake < 0.05) shake = 0; 158 }, 159 render() { 160 art.draw(ctx, { 161 world, 162 scrollX, 163 time, 164 flapTimer, 165 best, 166 newBest, 167 reducedMotion, 168 }); 169 const ox = shake > 0 ? (Math.random() - 0.5) * 2 * shake : 0; 170 const oy = shake > 0 ? (Math.random() - 0.5) * 2 * shake : 0; 171 display.present(ox, oy); 172 }, 173 }); 174 175 return { 176 pause() { 177 if (!destroyed) loop.pause(); 178 }, 179 resume() { 180 if (!destroyed) loop.resume(); 181 }, 182 destroy() { 183 if (destroyed) return; 184 destroyed = true; 185 loop.stop(); 186 scoreFlow.destroy(); 187 input.dispose(); 188 display.destroy(); 189 document.removeEventListener('keydown', onKey); 190 elements.soundButton.removeEventListener('click', onSoundClick); 191 container.replaceChildren(); 192 }, 193 }; 194}
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.