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.