PageSourceSearch

https://playsout.com/games/Flappy%20Parrot/js/main.js

js playsout.com collected 2026-09-24 10:51:15 UTC 5,359 bytes, 194 lines download raw bytes

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.