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.