PageSourceSearch

https://playsout.com/games/Recycle%20Tycoon/js/main.js

js playsout.com collected 2026-09-24 10:51:17 UTC 8,669 bytes, 343 lines download raw bytes

1import {
2  createWorld,
3  updateWorld,
4  swing,
5  continueRound,
6  setAim,
7  nudgeAim,
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  playCatch,
16  playBonus,
17  playRound,
18  playSplat,
19  playSwing,
20  resume as resumeAudio,
21  startMusic,
22  stopMusic,
23  toggleMuted,
24} from './audio.js';
25import {
26  assertGameMountOptions,
27  createGameShell,
28  createScoreFlow,
29} from '../../shared/game-integration.js?v=20260911-debug';
30
31const GAME = Object.freeze({
32  gameId: 4,
33  slug: 'recycle-tycoon',
34  title: 'RECYCLE TYCOON',
35  hint: 'click to catch · M mute · R restart',
36});
37const KEY_AXIS = {
38  ArrowUp: [0, -1],
39  ArrowDown: [0, 1],
40  ArrowLeft: [-1, 0],
41  ArrowRight: [1, 0],
42  w: [0, -1],
43  s: [0, 1],
44  a: [-1, 0],
45  d: [1, 0],
46  W: [0, -1],
47  S: [0, 1],
48  A: [-1, 0],
49  D: [1, 0],
50};
51
52export function mountRecycleTycoon(options) {
53  assertGameMountOptions(options, GAME);
54
55  const { container, auth, onExit } = options;
56  const elements = createGameShell(container, GAME);
57  const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false;
58  const { width, height } = viewSize();
59  const display = createDisplay(elements.stage, {
60    width,
61    height,
62    background: '#080a10',
63    backgroundGradient: ['#263b5b', '#080a10', '#4a2b38'],
64  });
65  const input = attachInput(elements.stage);
66  const art = createArt();
67  const ctx = display.ctx;
68  const held = new Set();
69
70  let world = createWorld({ seed: (Date.now() / 1000) | 0 });
71  let cover = true;
72  let time = 0;
73  let shake = 0;
74  let swingPulse = 0;
75  let displayed = 0;
76  let newBest = false;
77  let best = readBest();
78  let musicStarted = false;
79  let runStartedAt = Date.now();
80  let destroyed = false;
81  let scoreFlow;
82
83  function syncMuteButton() {
84    const muted = isMuted();
85    elements.soundButton.setAttribute('aria-pressed', String(!muted));
86    const label = elements.soundButton.querySelector('.toggle-text');
87    if (label) label.textContent = muted ? 'Sound off' : 'Sound on';
88  }
89
90  function startMusicOnce() {
91    if (musicStarted) return;
92    musicStarted = true;
93    startMusic();
94  }
95
96  function chase(current, target, dt) {
97    if (current === target) return target;
98    const diff = target - current;
99    const abs = Math.abs(diff);
100    const step = Math.max(abs * 9 * dt, 40 * dt * Math.max(1, abs / 400));
101    return abs <= step ? target : current + Math.sign(diff) * step;
102  }
103
104  function resetToCover() {
105    if (destroyed) return;
106    world = createWorld({ seed: (Date.now() / 1000) | 0 });
107    art.clearParticles();
108    held.clear();
109    cover = true;
110    shake = 0;
111    swingPulse = 0;
112    displayed = 0;
113    newBest = false;
114    runStartedAt = Date.now();
115    elements.stage.focus();
116  }
117
118  function restartPlaying() {
119    if (destroyed) return;
120    world = createWorld({ seed: (Date.now() / 1000) | 0 });
121    art.clearParticles();
122    held.clear();
123    cover = false;
124    shake = 0;
125    swingPulse = 0;
126    displayed = 0;
127    newBest = false;
128    runStartedAt = Date.now();
129    elements.stage.focus();
130  }
131
132  function noteScore() {
133    if (submitBest(undefined, world.saved)) {
134      newBest = true;
135      best = world.saved;
136    }
137  }
138
139  scoreFlow = createScoreFlow({
140    gameId: GAME.gameId,
141    auth,
142    elements,
143    onExit,
144    onPlayAgain: restartPlaying,
145  });
146
147  function handleEvents() {
148    for (const event of world.events) {
149      if (event.type === 'hit') {
150        playCatch(event.itemType);
151        const power = event.value >= 100 ? 2 : event.value >= 20 ? 1.5 : 1;
152        art.burst(event.x + 8, event.y + 6, '#f5d97a', power);
153        art.spawnFloat(event.x, event.y, `+${event.value}`);
154        if (!reducedMotion) shake = Math.max(shake, 1.4 * power);
155        noteScore();
156      } else if (event.type === 'bonus') {
157        playBonus();
158        art.burst(event.x + 7, event.y, '#ffe680', 2.4);
159        art.spawnFloat(event.x, event.y, `+${event.value}`);
160        if (!reducedMotion) shake = Math.max(shake, 3.2);
161        noteScore();
162      } else if (event.type === 'escaped' && event.splat && world.impact) {
163        playSplat();
164        art.splat(world.impact.x, world.impact.y, '#4aa564');
165        if (!reducedMotion) shake = Math.max(shake, 1.1);
166      } else if (event.type === 'roundOver') {
167        playRound();
168        noteScore();
169      } else if (event.type === 'gameOver') {
170        noteScore();
171        scoreFlow.completeRun(world.saved, runStartedAt);
172      }
173    }
174    world.events.length = 0;
175    for (const gone of world.bonusExpired) {
176      art.burst(gone.x, gone.y, '#f5d97a', 1.1);
177    }
178    world.bonusExpired.length = 0;
179    world.impact = null;
180    world.bonusHit = null;
181  }
182
183  function tapAt(x, y) {
184    if (destroyed || scoreFlow.isBlocking()) return;
185    resumeAudio();
186    startMusicOnce();
187    setAim(world, x, y);
188    if (cover) {
189      cover = false;
190      runStartedAt = Date.now();
191      return;
192    }
193    if (world.phase === 'roundOver') {
194      continueRound(world);
195      return;
196    }
197    if (world.phase === 'gameOver') {
198      restartPlaying();
199      return;
200    }
201    const result = swing(world, x, y);
202    if (result.result !== 'ignored') {
203      playSwing();
204      swingPulse = 1;
205    }
206  }
207
208  function onKey(event) {
209    const key = event.key;
210    if (key === 'm' || key === 'M') {
211      toggleMuted();
212      syncMuteButton();
213      resumeAudio();
214      startMusicOnce();
215      return;
216    }
217    if (key === 'r' || key === 'R') {
218      if (!scoreFlow.isBlocking()) resetToCover();
219      return;
220    }
221    if (scoreFlow.isBlocking()) return;
222    const axis = KEY_AXIS[key];
223    if (axis) {
224      event.preventDefault();
225      held.add(key);
226      resumeAudio();
227      return;
228    }
229    if (key === ' ' || key === 'Enter') {
230      event.preventDefault();
231      if (event.repeat) return;
232      resumeAudio();
233      startMusicOnce();
234      if (cover) {
235        cover = false;
236        runStartedAt = Date.now();
237        return;
238      }
239      if (world.phase === 'roundOver') continueRound(world);
240    }
241  }
242
243  function onKeyUp(event) {
244    held.delete(event.key);
245  }
246
247  function onBlur() {
248    held.clear();
249  }
250
251  function onSoundClick() {
252    resumeAudio();
253    startMusicOnce();
254    toggleMuted();
255    syncMuteButton();
256  }
257
258  window.addEventListener('keyup', onKeyUp);
259  window.addEventListener('blur', onBlur);
260  document.addEventListener('keydown', onKey);
261  elements.soundButton.addEventListener('click', onSoundClick);
262  syncMuteButton();
263
264  const loop = startLoop({
265    update(dt) {
266      time += dt;
267      if (scoreFlow.isBlocking()) {
268        held.clear();
269        input.takeClicks();
270        return;
271      }
272      const ptr = input.getPointer();
273      if (ptr) {
274        const pos = display.toGame(ptr.clientX, ptr.clientY);
275        setAim(world, pos.x, pos.y);
276      }
277      let ax = 0;
278      let ay = 0;
279      for (const key of held) {
280        const axis = KEY_AXIS[key];
281        if (axis) {
282          ax += axis[0];
283          ay += axis[1];
284        }
285      }
286      if (ax || ay) nudgeAim(world, dt, Math.sign(ax), Math.sign(ay));
287      for (const click of input.takeClicks()) {
288        const pos = display.toGame(click.clientX, click.clientY);
289        tapAt(pos.x, pos.y);
290      }
291      if (!cover) updateWorld(world, dt);
292      handleEvents();
293      art.updateParticles(dt);
294      displayed = chase(displayed, world.saved, dt);
295      swingPulse = Math.max(0, swingPulse - dt * 6);
296      if (!reducedMotion) {
297        shake *= Math.max(0, 1 - 9 * dt);
298        if (shake < 0.05) shake = 0;
299      } else {
300        shake = 0;
301      }
302    },
303    render() {
304      art.draw(ctx, {
305        world,
306        time,
307        best,
308        displayed,
309        cover,
310        newBest,
311        reducedMotion,
312        swingPulse,
313      });
314      const ox = shake > 0 ? (Math.random() - 0.5) * 2 * shake : 0;
315      const oy = shake > 0 ? (Math.random() - 0.5) * 2 * shake : 0;
316      display.present(ox, oy);
317    },
318  });
319
320  return {
321    pause() {
322      if (!destroyed) loop.pause();
323    },
324    resume() {
325      if (!destroyed) loop.resume();
326    },
327    destroy() {
328      if (destroyed) return;
329      destroyed = true;
330      loop.stop();
331      scoreFlow.destroy();
332      input.dispose();
333      display.destroy();
334      held.clear();
335      stopMusic();
336      window.removeEventListener('keyup', onKeyUp);
337      window.removeEventListener('blur', onBlur);
338      document.removeEventListener('keydown', onKey);
339      elements.soundButton.removeEventListener('click', onSoundClick);
340      container.replaceChildren();
341    },
342  };
343}

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.