PageSourceSearch

https://pupwife.com/scripts/dependencies/fishFood.js

js pupwife.com collected 2026-09-30 15:04:26 UTC 6,789 bytes, 217 lines download raw bytes

1// 🐠 Fish Food System w/ Bubble Burst
2const FISH_FOOD_SIZE = 12; // pixels
3const FISH_FOOD_SINK_SPEED = 0.55; // pixels per frame
4const FISH_FOOD_WIGGLE = 1.2; // horizontal wobble
5const FISH_FOOD_SPAWN_INTERVAL_MIN = 1500; // ms
6const FISH_FOOD_SPAWN_INTERVAL_MAX = 5000; // ms
7const NOM_SOUND_URLS = [
8  "https://cdn.pixabay.com/audio/2024/01/03/audio_d3cebd844e.mp3",
9  "https://cdn.pixabay.com/audio/2024/01/16/audio_f0aa2ad2dc.mp3",
10  "https://cdn.pixabay.com/audio/2024/01/16/audio_fb4eef1715.mp3",
11  "https://cdn.pixabay.com/audio/2024/01/16/audio_c3e0358182.mp3"
12];
13
14let fishFood = null;
15let fishFoodEnabled = true;
16let pendingSpawnTimer = null;
17
18function clearPendingSpawn() {
19  if (pendingSpawnTimer !== null) {
20    clearTimeout(pendingSpawnTimer);
21    pendingSpawnTimer = null;
22  }
23}
24
25function scheduleSpawn(delay) {
26  clearPendingSpawn();
27  if (!fishFoodEnabled) return;
28  pendingSpawnTimer = setTimeout(function () {
29    pendingSpawnTimer = null;
30    spawnFishFood();
31  }, delay);
32}
33
34function setFishFoodEnabled(enabled) {
35  fishFoodEnabled = !!enabled;
36  if (!fishFoodEnabled) {
37    clearPendingSpawn();
38    if (fishFood) {
39      fishFood.remove();
40      fishFood = null;
41    }
42    document.querySelectorAll('.fish-food, .bubble-pop').forEach(function (node) {
43      node.remove();
44    });
45  } else if (!fishFood && !pendingSpawnTimer) {
46    scheduleSpawn(randomSpawnInterval());
47  }
48}
49
50// Cursor position (used for collection when there's no 3D fish)
51let cursorX = null;
52let cursorY = null;
53document.addEventListener("mousemove", (e) => {
54  cursorX = e.clientX;
55  cursorY = e.clientY;
56});
57
58// Play one of the nom sounds at a slightly randomized volume (0.1 - 0.2)
59function playNomSound(){
60  try {
61    const url = NOM_SOUND_URLS[Math.floor(Math.random() * NOM_SOUND_URLS.length)];
62    const a = new Audio(url);
63    a.preload = 'auto';
64    a.volume = Math.random() * 0.1 + 0.1; // 0.1 .. 0.2
65    // auto-cleanup when finished
66    a.addEventListener('ended', () => { try { a.remove(); } catch(e){} });
67    a.play().catch(()=>{});
68  } catch(e){ /* ignore playback errors */ }
69}
70let spawnTime = 0;
71
72// 🎣 Helper: random position in viewport
73function randomPosition() {
74  const x = Math.random() * (window.innerWidth - FISH_FOOD_SIZE * 2) + FISH_FOOD_SIZE;
75  const y = Math.random() * (window.innerHeight / 3);
76  return { x, y };
77}
78
79// 🎣 Helper: random spawn interval
80function randomSpawnInterval() {
81  return Math.random() * (FISH_FOOD_SPAWN_INTERVAL_MAX - FISH_FOOD_SPAWN_INTERVAL_MIN) + FISH_FOOD_SPAWN_INTERVAL_MIN;
82}
83
84// 🍤 Spawn a pellet
85function spawnFishFood() {
86  if (!fishFoodEnabled) return;
87  if (fishFood) fishFood.remove();
88
89  const { x, y } = randomPosition();
90  const food = document.createElement("div");
91  food.classList.add("fish-food");
92  food.style.left = `${x}px`;
93  food.style.top = `${y}px`;
94  food.dataset.baseX = x;
95  food.dataset.offset = Math.random() * 1000;
96  document.body.appendChild(food);
97  fishFood = food;
98  spawnTime = Date.now();
99}
100
101// 🪱 Check collision: cursor or 3D fish (fishBuddy sets window.fishX/fishY when present)
102const COLLECT_RADIUS = 12; // pixels
103
104function checkCollision() {
105  // Use cursor position if available; otherwise fall back to 3D fish position
106  const px = cursorX ?? window.fishX;
107  const py = cursorY ?? window.fishY;
108  if (!fishFood || px === undefined || py === undefined) return;
109
110  const rect = fishFood.getBoundingClientRect();
111  const fx = rect.left + rect.width / 2;
112  const fy = rect.top + rect.height / 2;
113  const dx = fx - px;
114  const dy = fy - py;
115  const dist = Math.sqrt(dx * dx + dy * dy);
116
117  if (dist < COLLECT_RADIUS) collectFishFood(fx, fy);
118}
119
120// 🫧 Create bubble burst at location
121function createBubbleBurst(x, y) {
122  const bubbleCount = 2 + Math.floor(Math.random() * 2);
123
124  for (let i = 0; i < bubbleCount; i++) {
125    const bubble = document.createElement("div");
126    bubble.classList.add("bubble-pop");
127
128    // random small size + angle
129    const size = 6 + Math.random() * 6;
130    bubble.style.width = `${size}px`;
131    bubble.style.height = `${size}px`;
132    bubble.style.left = `${x}px`;
133    bubble.style.top = `${y}px`;
134    bubble.style.setProperty("--dx", (Math.random() - 0.5) * 60 + "px");
135    bubble.style.setProperty("--dy", (Math.random() - 1.2) * 60 + "px");
136    bubble.style.setProperty("--fade", (0.8 + Math.random() * 0.4).toFixed(2));
137
138    document.body.appendChild(bubble);
139
140    // cleanup
141    setTimeout(() => bubble.remove(), 800);
142  }
143}
144
145// 🫧 Handle collection
146function collectFishFood(x, y) {
147  if (!fishFood) return;
148  // guard so a single piece of food only triggers one collection flow
149  if (fishFood.dataset && fishFood.dataset.collected === '1') return;
150  if (fishFood.dataset) fishFood.dataset.collected = '1';
151  // play a randomized nom sound immediately
152  playNomSound();
153
154  createBubbleBurst(x, y);
155
156  // Emit a global event so other scripts (food counter UI, analytics, etc.) can react
157  try {
158    window.dispatchEvent(new CustomEvent('fishfood:collected', { detail: { when: Date.now() } }));
159  } catch (e) {
160    // ignore if dispatch fails in some environments
161  }
162
163  fishFood.style.transition = "opacity 0.3s ease-out, transform 0.3s ease-out";
164  fishFood.style.opacity = "0";
165  fishFood.style.transform = "scale(0.5)";
166
167  setTimeout(() => {
168    if (fishFood) {
169      fishFood.remove();
170      fishFood = null;
171      scheduleSpawn(randomSpawnInterval());
172    }
173  }, 300);
174}
175
176// 🫧 Animate sinking + wobble
177function animateFood() {
178  if (fishFood) {
179    let top = parseFloat(fishFood.style.top);
180    let baseX = parseFloat(fishFood.dataset.baseX);
181    let offset = parseFloat(fishFood.dataset.offset);
182
183    top += FISH_FOOD_SINK_SPEED;
184    const wobble = Math.sin((Date.now() + offset) / 400) * FISH_FOOD_WIGGLE;
185
186    fishFood.style.top = `${top}px`;
187    fishFood.style.left = `${baseX + wobble}px`;
188
189    if (top > window.innerHeight - 30) {
190      fishFood.style.transition = "opacity 0.5s ease-out";
191      fishFood.style.opacity = "0";
192      setTimeout(() => {
193        if (fishFood) {
194          fishFood.remove();
195          fishFood = null;
196          scheduleSpawn(randomSpawnInterval());
197        }
198      }, 500);
199    }
200  }
201
202  checkCollision();
203  requestAnimationFrame(animateFood);
204}
205
206// 🐟 Initialize
207// Start animation loop immediately
208animateFood();
209
210// Spawn first food after random initial delay (5-10 seconds)
211const initialDelay = Math.random() * 5000 + 5000; // 5000ms to 10000ms
212scheduleSpawn(initialDelay);
213
214window.FishFood = {
215  setEnabled: setFishFoodEnabled,
216  isEnabled: function () { return fishFoodEnabled; }
217};

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.