PageSourceSearch

https://playsout.com/games/Rio%20Rush/js/main.js

js playsout.com collected 2026-09-24 10:51:18 UTC 6,253 bytes, 238 lines download raw bytes

1import {
2  RULES,
3  createWorld,
4  startGame,
5  queueTurn,
6  updateWorld,
7  tapDirection,
8  viewSize,
9} from './world.js';
10import { readBest, submitBest } from './score.js';
11import { attachInput, createDisplay, startLoop } from './runtime.js';
12import { createArt } from './art.js';
13import {
14  isMuted,
15  playBust,
16  playCollar,
17  playStart,
18  playTurn,
19  resume as resumeAudio,
20  toggleMuted,
21} from './audio.js';
22import {
23  assertGameMountOptions,
24  createGameShell,
25  createScoreFlow,
26} from '../../shared/game-integration.js?v=20260911-debug';
27
28const GAME = Object.freeze({
29  gameId: 2,
30  slug: 'rio-rush',
31  title: 'RIO RUSH',
32  hint: 'tap or arrows to steer · M mute · R restart',
33});
34const KEY_DIR = {
35  ArrowUp: [0, -1],
36  ArrowDown: [0, 1],
37  ArrowLeft: [-1, 0],
38  ArrowRight: [1, 0],
39  w: [0, -1],
40  s: [0, 1],
41  a: [-1, 0],
42  d: [1, 0],
43  W: [0, -1],
44  S: [0, 1],
45  A: [-1, 0],
46  D: [1, 0],
47};
48
49export function mountRioRush(options) {
50  assertGameMountOptions(options, GAME);
51
52  const { container, auth, onExit } = options;
53  const elements = createGameShell(container, GAME);
54  const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false;
55  const { width, height } = viewSize();
56  const display = createDisplay(elements.stage, {
57    width,
58    height,
59    background: '#0d1017',
60    backgroundGradient: ['#1e3a5f', '#0d1017', '#4a202c'],
61  });
62  const input = attachInput(elements.stage);
63  const art = createArt();
64  const ctx = display.ctx;
65
66  let world = createWorld({ seed: (Date.now() / 1000) | 0 });
67  let time = 0;
68  let shake = 0;
69  let newBest = false;
70  let best = readBest();
71  let runStartedAt = Date.now();
72  let destroyed = false;
73  let scoreFlow;
74
75  function syncMuteButton() {
76    const muted = isMuted();
77    elements.soundButton.setAttribute('aria-pressed', String(!muted));
78    const label = elements.soundButton.querySelector('.toggle-text');
79    if (label) label.textContent = muted ? 'Sound off' : 'Sound on';
80  }
81
82  function reset() {
83    if (destroyed) return;
84    world = createWorld({ seed: (Date.now() / 1000) | 0 });
85    art.clearParticles();
86    shake = 0;
87    newBest = false;
88    runStartedAt = Date.now();
89    elements.stage.focus();
90  }
91
92  scoreFlow = createScoreFlow({
93    gameId: GAME.gameId,
94    auth,
95    elements,
96    onExit,
97    onPlayAgain: reset,
98  });
99
100  function tryStart() {
101    if (destroyed || scoreFlow.isBlocking()) return;
102    resumeAudio();
103    if (world.phase === 'dead' || world.phase === 'cleared') {
104      if (world.t >= RULES.restartDelay) reset();
105      return;
106    }
107    if (startGame(world)) playStart();
108  }
109
110  function tryTurn(dx, dy) {
111    if (destroyed || scoreFlow.isBlocking()) return;
112    resumeAudio();
113    if (world.phase === 'dead' || world.phase === 'cleared') {
114      if (world.t >= RULES.restartDelay) reset();
115      return;
116    }
117    const fromReady = world.phase === 'ready';
118    if (queueTurn(world, dx, dy) && fromReady) playStart();
119  }
120
121  function handlePointer(pos) {
122    if (destroyed || scoreFlow.isBlocking()) return;
123    resumeAudio();
124    if (world.phase === 'dead' || world.phase === 'cleared') {
125      if (world.t >= RULES.restartDelay) reset();
126      return;
127    }
128    if (world.phase === 'ready') {
129      tryStart();
130      return;
131    }
132    const dir = tapDirection(world, pos.x, pos.y);
133    if (dir) queueTurn(world, dir.x, dir.y);
134  }
135
136  function onKey(event) {
137    const key = event.key;
138    if (key === 'm' || key === 'M') {
139      toggleMuted();
140      syncMuteButton();
141      resumeAudio();
142      return;
143    }
144    if (key === 'r' || key === 'R') {
145      if (!scoreFlow.isBlocking()) reset();
146      return;
147    }
148    const dir = KEY_DIR[key];
149    if (dir) {
150      event.preventDefault();
151      if (!event.repeat) tryTurn(dir[0], dir[1]);
152      return;
153    }
154    if (key === ' ' || key === 'Enter') {
155      event.preventDefault();
156      if (!event.repeat) tryStart();
157    }
158  }
159
160  function onSoundClick() {
161    resumeAudio();
162    toggleMuted();
163    syncMuteButton();
164  }
165
166  elements.soundButton.addEventListener('click', onSoundClick);
167  document.addEventListener('keydown', onKey);
168  syncMuteButton();
169
170  const loop = startLoop({
171    update(dt) {
172      time += dt;
173      if (scoreFlow.isBlocking()) {
174        input.takeClicks();
175        return;
176      }
177      for (const click of input.takeClicks()) {
178        const pos = display.toGame(click.clientX, click.clientY);
179        handlePointer(pos);
180      }
181      updateWorld(world, dt);
182      for (const event of world.events) {
183        if (event.type === 'turn') {
184          playTurn();
185        } else if (event.type === 'collar') {
186          playCollar();
187          art.pulse(1);
188          const at = art.cellCenter(event.x, event.y);
189          art.burst(at.x, at.y, event.shirt);
190          art.spawnFloat(at.x, at.y - 8, `+${event.gained}`, '#f5c542');
191          if (submitBest(undefined, event.score)) {
192            newBest = true;
193            best = event.score;
194          }
195        } else if (event.type === 'bust') {
196          playBust();
197          const at = art.cellCenter(event.x, event.y);
198          art.burst(at.x, at.y, 'green');
199          if (!reducedMotion) shake = 4.5;
200          scoreFlow.completeRun(event.score, runStartedAt);
201        } else if (event.type === 'cleared') {
202          playCollar();
203          scoreFlow.completeRun(event.score, runStartedAt);
204        }
205      }
206      world.events.length = 0;
207      art.updateParticles(dt);
208      shake *= Math.max(0, 1 - 9 * dt);
209      if (shake < 0.05) shake = 0;
210    },
211    render() {
212      art.draw(ctx, { world, time, best, newBest, reducedMotion });
213      const ox = shake > 0 ? (Math.random() - 0.5) * 2 * shake : 0;
214      const oy = shake > 0 ? (Math.random() - 0.5) * 2 * shake : 0;
215      display.present(ox, oy);
216    },
217  });
218
219  return {
220    pause() {
221      if (!destroyed) loop.pause();
222    },
223    resume() {
224      if (!destroyed) loop.resume();
225    },
226    destroy() {
227      if (destroyed) return;
228      destroyed = true;
229      loop.stop();
230      scoreFlow.destroy();
231      input.dispose();
232      display.destroy();
233      document.removeEventListener('keydown', onKey);
234      elements.soundButton.removeEventListener('click', onSoundClick);
235      container.replaceChildren();
236    },
237  };
238}

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.