vod frog, frog with the vods
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

settings modal with sound and pacifist mode toggles, persisted to localStorage, wavy toggle buttons

+144 -13
+8 -5
src/lib/FlySpawner.svelte
··· 6 6 --> 7 7 <script lang="ts"> 8 8 import { onMount, onDestroy } from 'svelte'; 9 + import { getSettings } from './settings.svelte'; 9 10 11 + const settings = getSettings(); 10 12 const MAX_FLIES = 5; 11 13 const MIN_SPAWN_MS = 10000; 12 14 const MAX_SPAWN_MS = 120000; ··· 53 55 } 54 56 55 57 function spawnFly() { 56 - if (flies.length >= MAX_FLIES) { 58 + if (settings.pacifistMode || flies.length >= MAX_FLIES) { 57 59 scheduleNextSpawn(); 58 60 return; 59 61 } ··· 81 83 } 82 84 83 85 function splatFly(id: number) { 84 - // Play splat sound (clone so overlapping splats all play) 85 - const sfx = splatAudio.cloneNode() as HTMLAudioElement; 86 - sfx.volume = 0.5; 87 - sfx.play().catch(() => {}); 86 + if (settings.soundEnabled) { 87 + const sfx = splatAudio.cloneNode() as HTMLAudioElement; 88 + sfx.volume = 0.5; 89 + sfx.play().catch(() => {}); 90 + } 88 91 flies = flies.map(f => f.id === id && !f.splatted ? { ...f, splatted: true } : f); 89 92 90 93 setTimeout(() => {
+90 -8
src/lib/FrogHeader.svelte
··· 6 6 --> 7 7 <script lang="ts"> 8 8 import LoginButton from './LoginButton.svelte'; 9 + import WavyButton from './WavyButton.svelte'; 9 10 import { playCroak } from './croak'; 11 + import { getSettings, setSoundEnabled, setPacifistMode } from './settings.svelte'; 10 12 11 13 let { onHomeClick }: { onHomeClick?: () => void } = $props(); 14 + const settings = getSettings(); 12 15 let menuOpen = $state(false); 13 16 let showCreditsModal = $state(false); 17 + let showSettingsModal = $state(false); 14 18 15 19 function handleClick(e: MouseEvent) { 16 20 if (onHomeClick) { ··· 34 38 function openCredits() { 35 39 playCroak(); 36 40 showCreditsModal = true; 41 + showSettingsModal = false; 37 42 menuOpen = false; 38 43 } 39 44 ··· 41 46 showCreditsModal = false; 42 47 } 43 48 49 + function openSettings() { 50 + playCroak(); 51 + showSettingsModal = true; 52 + showCreditsModal = false; 53 + menuOpen = false; 54 + } 55 + 56 + function closeSettings() { 57 + showSettingsModal = false; 58 + } 59 + 44 60 function onBackdropClick(e: MouseEvent) { 45 - if (e.target === e.currentTarget) closeCredits(); 61 + if (e.target === e.currentTarget) { 62 + closeCredits(); 63 + closeSettings(); 64 + } 46 65 } 47 66 48 67 function onKeyDown(e: KeyboardEvent) { 49 - if (e.key === 'Escape') closeCredits(); 68 + if (e.key === 'Escape') { 69 + closeCredits(); 70 + closeSettings(); 71 + } 50 72 } 51 73 </script> 52 74 ··· 71 93 /> 72 94 <!-- Mobile: menu drops down directly under the frog --> 73 95 <div class="lily-menu mobile-lily" class:open={menuOpen}> 74 - <button class="lily-item" onclick={() => { playCroak(); }}> 96 + <button class="lily-item" onclick={openSettings}> 75 97 <img src="/lilymenu.svg" alt="" class="lily-img" /> 76 98 <span class="lily-label">settings</span> 77 99 </button> ··· 91 113 92 114 <!-- Desktop: lilypad menu fixed bottom-left --> 93 115 <div class="lily-menu desktop-lily"> 94 - <button class="lily-item" onclick={() => { playCroak(); }}> 116 + <button class="lily-item" onclick={openSettings}> 95 117 <img src="/lilymenu.svg" alt="" class="lily-img" /> 96 118 <span class="lily-label">settings</span> 97 119 </button> ··· 105 127 <!-- svelte-ignore a11y_no_static_element_interactions --> 106 128 <div class="modal-backdrop" onclick={onBackdropClick}> 107 129 <div class="modal-content"> 108 - <div class="credits-body"> 109 - <h2 class="credits-title">credits</h2> 130 + <div class="modal-body"> 131 + <h2 class="modal-title">credits</h2> 110 132 <div class="credit-item"> 111 133 <p class="credit-label">Frog Croaking</p> 112 134 <p class="credit-author">by DrinkingWindGames</p> ··· 121 143 </div> 122 144 {/if} 123 145 146 + {#if showSettingsModal} 147 + <!-- svelte-ignore a11y_no_static_element_interactions --> 148 + <div class="modal-backdrop" onclick={onBackdropClick}> 149 + <div class="modal-content"> 150 + <div class="modal-body"> 151 + <h2 class="modal-title">settings</h2> 152 + 153 + <div class="setting-row"> 154 + <span class="setting-label">sound</span> 155 + <WavyButton 156 + seed="toggle-sound" 157 + fill={settings.soundEnabled ? '#39FF44' : '#0A182B'} 158 + textColor={settings.soundEnabled ? '#0A182B' : '#FFDEED'} 159 + onclick={() => setSoundEnabled(!settings.soundEnabled)} 160 + >{settings.soundEnabled ? 'on' : 'off'}</WavyButton> 161 + </div> 162 + 163 + <div class="setting-row"> 164 + <span class="setting-label">pacifist mode</span> 165 + <WavyButton 166 + seed="toggle-pacifist" 167 + fill={settings.pacifistMode ? '#39FF44' : '#0A182B'} 168 + textColor={settings.pacifistMode ? '#0A182B' : '#FFDEED'} 169 + onclick={() => setPacifistMode(!settings.pacifistMode)} 170 + >{settings.pacifistMode ? 'on' : 'off'}</WavyButton> 171 + </div> 172 + 173 + <p class="setting-hint">pacifist mode disables the flies</p> 174 + 175 + <button class="close-btn" onclick={closeSettings}>close</button> 176 + </div> 177 + </div> 178 + </div> 179 + {/if} 180 + 124 181 <style> 125 182 .frog-header { 126 183 padding: 30px 20px 50px; ··· 246 303 padding: 32px 24px; 247 304 } 248 305 249 - .credits-body { 306 + .modal-body { 250 307 display: flex; 251 308 flex-direction: column; 252 309 align-items: center; ··· 254 311 gap: 12px; 255 312 } 256 313 257 - .credits-title { 314 + .modal-title { 258 315 font-family: 'PicNic', cursive, system-ui; 259 316 font-size: clamp(1.6rem, 3.5vw, 2.2rem); 260 317 color: #0A182B; 261 318 margin: 0; 319 + } 320 + 321 + /* Settings */ 322 + .setting-row { 323 + display: flex; 324 + align-items: center; 325 + justify-content: space-between; 326 + width: min(280px, 70vw); 327 + gap: 16px; 328 + } 329 + 330 + .setting-label { 331 + font-family: 'PicNic', cursive, system-ui; 332 + font-size: 1.1rem; 333 + color: #0A182B; 334 + } 335 + 336 + 337 + .setting-hint { 338 + font-family: 'Fang', system-ui, sans-serif; 339 + font-size: 0.75rem; 340 + color: #0A182B; 341 + opacity: 0.5; 342 + margin: 0; 343 + font-style: italic; 262 344 } 263 345 264 346 .credit-item {
+4
src/lib/croak.ts
··· 1 1 /** 2 2 * Croak SFX — plays a random frog croak when called. 3 3 * Pre-loads all 11 croak mp3s and picks one at random each time. 4 + * Respects the global sound setting. 4 5 */ 6 + 7 + import { getSettings } from './settings.svelte'; 5 8 6 9 const CROAK_COUNT = 11; 7 10 const croakPaths = Array.from({ length: CROAK_COUNT }, (_, i) => `/sfx/croak${i + 1}.mp3`); 8 11 9 12 /** Play a random croak sound effect. Volume is kept moderate so it's fun, not startling. */ 10 13 export function playCroak() { 14 + if (!getSettings().soundEnabled) return; 11 15 const idx = Math.floor(Math.random() * CROAK_COUNT); 12 16 const audio = new Audio(croakPaths[idx]); 13 17 audio.volume = 0.4;
+42
src/lib/settings.svelte.ts
··· 1 + /** 2 + * Global app settings, persisted to localStorage. 3 + * - soundEnabled: controls all SFX (croaks, splats) 4 + * - pacifistMode: disables fly spawning 5 + */ 6 + 7 + const STORAGE_KEY = 'vodfrog-settings'; 8 + 9 + interface Settings { 10 + soundEnabled: boolean; 11 + pacifistMode: boolean; 12 + } 13 + 14 + function loadSettings(): Settings { 15 + if (typeof localStorage === 'undefined') return { soundEnabled: true, pacifistMode: false }; 16 + try { 17 + const raw = localStorage.getItem(STORAGE_KEY); 18 + if (raw) return { ...{ soundEnabled: true, pacifistMode: false }, ...JSON.parse(raw) }; 19 + } catch { /* ignore */ } 20 + return { soundEnabled: true, pacifistMode: false }; 21 + } 22 + 23 + function saveSettings(s: Settings) { 24 + if (typeof localStorage === 'undefined') return; 25 + try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch { /* ignore */ } 26 + } 27 + 28 + let settings = $state<Settings>(loadSettings()); 29 + 30 + export function getSettings() { 31 + return settings; 32 + } 33 + 34 + export function setSoundEnabled(v: boolean) { 35 + settings.soundEnabled = v; 36 + saveSettings(settings); 37 + } 38 + 39 + export function setPacifistMode(v: boolean) { 40 + settings.pacifistMode = v; 41 + saveSettings(settings); 42 + }