PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:04:27 UTC 6,024 bytes, 223 lines download raw bytes

1// foodCounter.js
2// Shows a medium-sized counter in the bottom-right of the screen that increments
3// whenever a piece of fish food is collected. Listens for 'fishfood:collected' events.
4// Syncs count across browser tabs via localStorage + BroadcastChannel.
5
6(function () {
7  'use strict';
8
9  if (window.__puppyFoodCounter) return;
10
11  const STORAGE_KEY = 'puppy_food_count';
12  const SYNC_CHANNEL_NAME = 'pupwife_fish_food_sync';
13  const TAB_ID = Math.random().toString(36).slice(2);
14
15  let count = 0;
16  let canIncrement = true;
17  let syncChannel = null;
18
19  const el = document.createElement('div');
20  el.id = 'food-counter';
21  el.setAttribute('aria-live', 'polite');
22  el.style.position = 'fixed';
23  el.style.right = '18px';
24  el.style.bottom = '18px';
25  el.style.zIndex = '10000';
26  el.style.background = 'rgba(0,0,0,0.4)';
27  el.style.color = 'white';
28  el.style.padding = '8px 12px';
29  el.style.borderRadius = '8px';
30  el.style.fontFamily = "'Comic Sans MS', sans-serif";
31  el.style.fontSize = '28px';
32  el.style.fontWeight = '700';
33  el.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
34  el.style.pointerEvents = 'none';
35  el.style.userSelect = 'none';
36  el.textContent = '0';
37
38  function readStoredCount() {
39    try {
40      const stored = localStorage.getItem(STORAGE_KEY);
41      if (stored === null) return 0;
42      return parseInt(stored, 10) || 0;
43    } catch (e) {
44      return 0;
45    }
46  }
47
48  function renderCount() {
49    el.textContent = String(count);
50  }
51
52  function bump() {
53    el.animate(
54      [
55        { transform: 'scale(1)', offset: 0 },
56        { transform: 'scale(1.12)', offset: 0.3 },
57        { transform: 'scale(0.98)', offset: 0.6 },
58        { transform: 'scale(1)', offset: 1 }
59      ],
60      { duration: 360, easing: 'cubic-bezier(.2,.9,.2,1)' }
61    );
62  }
63
64  function broadcastCount(value) {
65    if (!syncChannel) return;
66    try {
67      syncChannel.postMessage({ tabId: TAB_ID, count: value });
68    } catch (e) {}
69  }
70
71  function persistCount(value, skipBroadcast) {
72    try {
73      localStorage.setItem(STORAGE_KEY, String(value));
74    } catch (e) {}
75    if (!skipBroadcast) {
76      broadcastCount(value);
77    }
78  }
79
80  /** Adopt a higher count from another tab (silent). */
81  function adoptPeerCount(peerCount, options) {
82    options = options || {};
83    const parsed = Math.max(0, parseInt(peerCount, 10) || 0);
84
85    if (options.allowReset && parsed === 0) {
86      if (count === 0) return false;
87      count = 0;
88      renderCount();
89      return true;
90    }
91
92    if (parsed <= count) return false;
93    count = parsed;
94    renderCount();
95    return true;
96  }
97
98  function initCrossTabSync() {
99    window.addEventListener('storage', function (e) {
100      if (e.key !== STORAGE_KEY) return;
101
102      if (e.newValue === null || e.newValue === '0') {
103        const oldVal = e.oldValue ? (parseInt(e.oldValue, 10) || 0) : 0;
104        if (oldVal > 0) {
105          adoptPeerCount(0, { allowReset: true });
106        }
107        return;
108      }
109
110      adoptPeerCount(parseInt(e.newValue, 10) || 0);
111    });
112
113    if (typeof BroadcastChannel === 'undefined') return;
114
115    try {
116      syncChannel = new BroadcastChannel(SYNC_CHANNEL_NAME);
117      syncChannel.onmessage = function (e) {
118        const data = e.data;
119        if (!data || data.tabId === TAB_ID) return;
120        if (typeof data.count !== 'number') return;
121        if (data.count === 0) {
122          adoptPeerCount(0, { allowReset: true });
123          return;
124        }
125        adoptPeerCount(data.count);
126      };
127    } catch (e) {
128      syncChannel = null;
129    }
130  }
131
132  function mountCounter() {
133    if (document.getElementById('food-counter')) return;
134    count = readStoredCount();
135    renderCount();
136    document.body.appendChild(el);
137    initCrossTabSync();
138    window.dispatchEvent(new CustomEvent('fishfood:sync-request'));
139  }
140
141  if (document.readyState === 'loading') {
142    document.addEventListener('DOMContentLoaded', mountCounter);
143  } else {
144    mountCounter();
145  }
146
147  function onCollected() {
148    if (!canIncrement) return;
149
150    const latest = Math.max(count, readStoredCount());
151    count = latest + 1;
152    renderCount();
153    persistCount(count);
154
155    bump();
156
157    canIncrement = false;
158    setTimeout(function () {
159      canIncrement = true;
160    }, 1000);
161  }
162
163  window.addEventListener('fishfood:collected', onCollected);
164
165  window.__puppyFoodCounter = {
166    get count() { return count; },
167    setVisible(visible) {
168      el.style.display = visible ? '' : 'none';
169    },
170    reset() {
171      count = 0;
172      renderCount();
173      try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
174      broadcastCount(0);
175    },
176    restore() {
177      try {
178        const restoredCount = readStoredCount();
179        if (restoredCount !== count) {
180          count = restoredCount;
181          renderCount();
182          bump();
183          if (typeof window.devLog === 'function') {
184            window.devLog('Fish food count restored to: ' + count);
185          }
186        }
187      } catch (e) {
188        console.warn('Failed to restore fish food count:', e);
189      }
190    },
191    setCount(newCount, options) {
192      options = options || {};
193      count = Math.max(0, parseInt(newCount, 10) || 0);
194      renderCount();
195      persistCount(count, !!options.skipBroadcast);
196      if (options.silent !== true) {
197        bump();
198      }
199    },
200    syncFromStorage() {
201      const stored = readStoredCount();
202      if (stored > count) {
203        count = stored;
204        renderCount();
205      }
206      return count;
207    }
208  };
209
210  window.addEventListener('fishfood:migrated', function () {
211    setTimeout(function () {
212      window.__puppyFoodCounter.restore();
213    }, 100);
214  });
215
216  if (!document.querySelector('script[data-fishfood-jumpscare]')) {
217    var jumpscareScript = document.createElement('script');
218    jumpscareScript.src = '/scripts/dependencies/fishFoodJumpscare.js';
219    jumpscareScript.setAttribute('data-fishfood-jumpscare', '1');
220    jumpscareScript.async = true;
221    (document.head || document.documentElement).appendChild(jumpscareScript);
222  }
223})();

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.