1import { 2 RULES, 3 createWorld, 4 startPlaying, 5 resetToIntro, 6 grab, 7 setClaw, 8 nudgeClaw, 9 updateWorld, 10 viewSize, 11} from './world.js'; 12import { readBest, submitBest } from './score.js'; 13import { attachInput, createDisplay, startLoop } from './runtime.js'; 14import { createArt } from './art.js'; 15import { 16 isMuted, 17 playCombo, 18 playEarly, 19 playImpact, 20 playOver, 21 playPull, 22 playShift, 23 playShip, 24 playThud, 25 playWrong, 26 resume as resumeAudio, 27 toggleMuted, 28} from './audio.js'; 29import { 30 assertGameMountOptions, 31 createGameShell, 32 createScoreFlow, 33} from '../../shared/game-integration.js?v=20260911-debug'; 34 35const GAME = Object.freeze({ 36 gameId: 3, 37 slug: 'parcel-sort', 38 title: 'PARCEL SORT', 39 hint: 'ç¹æå¤¹åæç© · M éé³ Â· R éå¼', 40}); 41const KEY_AXIS = { 42 ArrowUp: [0, -1], 43 ArrowDown: [0, 1], 44 ArrowLeft: [-1, 0], 45 ArrowRight: [1, 0], 46 w: [0, -1], 47 s: [0, 1], 48 a: [-1, 0], 49 d: [1, 0], 50 W: [0, -1], 51 S: [0, 1], 52 A: [-1, 0], 53 D: [1, 0], 54}; 55 56export function mountParcelSort(options) { 57 assertGameMountOptions(options, GAME); 58 59 const { container, auth, onExit } = options; 60 const elements = createGameShell(container, GAME); 61 const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false; 62 const { width, height } = viewSize(); 63 const display = createDisplay(elements.stage, { 64 width, 65 height, 66 background: '#0b0e11', 67 backgroundGradient: ['#32204f', '#0b0e11', '#123847'], 68 }); 69 const input = attachInput(elements.stage); 70 const art = createArt(); 71 const ctx = display.ctx; 72 const held = new Set(); 73 74 let world = createWorld({ seed: (Date.now() / 1000) | 0, intro: true }); 75 let time = 0; 76 let shake = 0; 77 let flash = 0; 78 let newBest = false; 79 let best = readBest(); 80 let runStartedAt = Date.now(); 81 let destroyed = false; 82 let scoreFlow; 83 84 function syncMuteButton() { 85 const muted = isMuted(); 86 elements.soundButton.setAttribute('aria-pressed', String(!muted)); 87 const label = elements.soundButton.querySelector('.toggle-text'); 88 if (label) label.textContent = muted ? 'Sound off' : 'Sound on'; 89 } 90 91 function startRun() { 92 startPlaying(world); 93 art.clearParticles(); 94 shake = 0; 95 flash = 0; 96 newBest = false; 97 runStartedAt = Date.now(); 98 } 99 100 function reset() { 101 if (destroyed) return; 102 resetToIntro(world); 103 art.clearParticles(); 104 held.clear(); 105 shake = 0; 106 flash = 0; 107 newBest = false; 108 runStartedAt = Date.now(); 109 elements.stage.focus(); 110 } 111 112 scoreFlow = createScoreFlow({ 113 gameId: GAME.gameId, 114 auth, 115 elements, 116 onExit, 117 onPlayAgain: reset, 118 }); 119 120 function handleEvents() { 121 for (const event of world.events) { 122 if (event.type === 'pull') { 123 playPull(); 124 art.burst(event.x + 8, event.y + 8, '#c39a4a'); 125 art.spawnFloat(event.x, event.y, `+${event.score}`, '#8fe08a'); 126 } else if (event.type === 'early') { 127 playEarly(); 128 art.spawnFloat(event.x, event.y - 7, 'EARLY', '#e8cf7a'); 129 } else if (event.type === 'ship') { 130 playShip(); 131 art.spawnFloat(event.x, event.y, `+${event.score}`, '#8fe08a'); 132 if (submitBest(undefined, world.score)) { 133 newBest = true; 134 best = world.score; 135 } 136 } else if (event.type === 'wrong') { 137 playWrong(); 138 art.spawnFloat(event.x, event.y, '-150', '#ff7a6e'); 139 if (!reducedMotion) flash = 0.5; 140 } else if (event.type === 'thud') { 141 playThud(); 142 if (!reducedMotion) { 143 flash = 0.7; 144 shake = 5; 145 } 146 art.burst(RULES.robotX + 20, RULES.robotY + 22, '#ff7a6e'); 147 art.spawnFloat(width / 2 - 8, 104, 'LET ONE THROUGH', '#ff7a6e'); 148 } else if (event.type === 'impact') { 149 playImpact(); 150 if (!reducedMotion) shake = 8; 151 } else if (event.type === 'shift') { 152 playShift(); 153 } else if (event.type === 'combo') { 154 playCombo(event.mult); 155 } else if (event.type === 'over') { 156 playOver(); 157 if (submitBest(undefined, world.score)) { 158 newBest = true; 159 best = world.score; 160 } 161 scoreFlow.completeRun(world.score, runStartedAt); 162 } 163 } 164 world.events.length = 0; 165 } 166 167 function tapAt(x, y) { 168 if (destroyed || scoreFlow.isBlocking()) return; 169 resumeAudio(); 170 setClaw(world, x, y); 171 if (world.phase !== 'playing') { 172 startRun(); 173 return; 174 } 175 grab(world, x, y); 176 } 177 178 function onKey(event) {
179 const key = event.key; 180 if (key === 'm' || key === 'M') { 181 toggleMuted(); 182 syncMuteButton(); 183 resumeAudio(); 184 return; 185 } 186 if (key === 'r' || key === 'R') { 187 if (!scoreFlow.isBlocking()) reset(); 188 return; 189 } 190 if (scoreFlow.isBlocking()) return; 191 const axis = KEY_AXIS[key]; 192 if (axis) { 193 event.preventDefault(); 194 held.add(key); 195 resumeAudio(); 196 return; 197 } 198 if (key === ' ' || key === 'Enter') { 199 event.preventDefault(); 200 if (event.repeat) return; 201 resumeAudio(); 202 if (world.phase !== 'playing') { 203 startRun(); 204 return; 205 } 206 if (world.px > -50) grab(world, world.px, world.py); 207 } 208 } 209 210 function onKeyUp(event) { 211 held.delete(event.key); 212 } 213 214 function onBlur() { 215 held.clear(); 216 } 217 218 function onSoundClick() { 219 resumeAudio(); 220 toggleMuted(); 221 syncMuteButton(); 222 } 223 224 window.addEventListener('keyup', onKeyUp); 225 window.addEventListener('blur', onBlur); 226 document.addEventListener('keydown', onKey); 227 elements.soundButton.addEventListener('click', onSoundClick); 228 syncMuteButton(); 229 230 const loop = startLoop({ 231 update(dt) { 232 time += dt; 233 if (scoreFlow.isBlocking()) { 234 held.clear(); 235 input.takeClicks(); 236 return; 237 } 238 const ptr = input.getPointer(); 239 if (ptr) {
240 const pos = display.toGame(ptr.clientX, ptr.clientY); 241 setClaw(world, pos.x, pos.y); 242 } 243 let ax = 0; 244 let ay = 0; 245 for (const key of held) { 246 const axis = KEY_AXIS[key]; 247 if (axis) { 248 ax += axis[0]; 249 ay += axis[1]; 250 } 251 } 252 if (ax || ay) nudgeClaw(world, dt, Math.sign(ax), Math.sign(ay)); 253 for (const click of input.takeClicks()) { 254 const pos = display.toGame(click.clientX, click.clientY); 255 tapAt(pos.x, pos.y); 256 } 257 updateWorld(world, dt); 258 handleEvents(); 259 art.updateParticles(dt); 260 if (!reducedMotion) { 261 shake = Math.max(0, shake - dt * 4.5); 262 flash = Math.max(0, flash - dt * 3.2); 263 } else { 264 shake = 0; 265 flash = 0; 266 } 267 }, 268 render() { 269 art.draw(ctx, { world, time, best, newBest, reducedMotion, flash }); 270 const ox = shake > 0 ? (Math.random() - 0.5) * 6 * Math.min(1, shake) : 0; 271 const oy = shake > 0 ? (Math.random() - 0.5) * 6 * Math.min(1, shake) : 0; 272 display.present(ox, oy); 273 }, 274 }); 275 276 return { 277 pause() { 278 if (!destroyed) loop.pause(); 279 }, 280 resume() { 281 if (!destroyed) loop.resume(); 282 }, 283 destroy() { 284 if (destroyed) return; 285 destroyed = true; 286 loop.stop(); 287 scoreFlow.destroy(); 288 input.dispose(); 289 display.destroy(); 290 held.clear(); 291 window.removeEventListener('keyup', onKeyUp); 292 window.removeEventListener('blur', onBlur); 293 document.removeEventListener('keydown', onKey); 294 elements.soundButton.removeEventListener('click', onSoundClick); 295 container.replaceChildren(); 296 }, 297 }; 298}
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.