PageSourceSearch

https://playsout.com/games/Parcel%20Sort/js/main.js

js playsout.com collected 2026-10-01 14:12:30 UTC 7,552 bytes, 298 lines download raw bytes

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.