1// --- 0. CONFIGURATION --- 2const DISTANCE_LOCK = 15; 3const MAX_GUESSES = 5; 4const ENABLE_MERCY_ARROW = true; // Toggle the directional arrow during live radar 5 6// --- PATH LINE STYLES --- 7const PATH_STYLES = { 8 dashed: { color: '#d9534f', weight: 3, opacity: 0.8, dashArray: '8, 8' }, 9 dotted: { color: '#00e5ff', weight: 4, opacity: 0.8, dashArray: '1, 8', lineCap: 'round', lineJoin: 'round' }, 10 solid: { color: '#d9534f', weight: 2, opacity: 0.5 } 11}; 12const CURRENT_PATH_STYLE = 'dashed'; // Change this string to swap the style! 13 14// --- TILE PROVIDERS DICTIONARY --- 15const MAP_STYLES = { 16 "satellite": { 17 base: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", 18 label: "https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}" 19 }, 20 "topo": { // Esri World Topo (Light, clean, topographical) 21 base: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}", 22 label: null // Labels are built-in 23 }, 24 "osm": { // Standard OpenStreetMap 25 base: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 26 label: null // Labels are built-in 27 }, 28 "light": { // Carto Light (Clean, minimalist) 29 base: "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png", 30 label: "https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}" 31 }, 32 "dark": { 33 base: "https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png", 34 label: "https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}" 35 } 36}; 37 38// --- 1. CORE STATE VARIABLES --- 39let gameWon = false, hintsUsed = 0, hintsTotal = 0, secondsElapsed = 0, timerInterval; 40let guessesMade = 0, lastMissDistanceMiles = null, worldData = null; 41let wordMapDatabase = [], todaysGame, todaysIndex, todayStr; 42let unit, timerEnabled, startLoc, mapStyle, hardModeEnabled, audioEnabled; 43let timerStarted = false, radarActive = false; 44let map, currentBaseLayer = null, currentLabelLayer = null; 45let audioCtx = null; 46 47// --- STATE VARS FOR GUESS PATH --- 48let guessPathCoords = []; 49let guessPathLayer = null; 50 51// --- DYNAMIC LAYOUT ENGINE --- 52function updateLayout() { 53 const hud = document.getElementById('hud-top'); 54 const crosshair = document.getElementById('crosshair'); 55 56 if (!hud || hud.style.display === 'none') { 57 crosshair.style.top = '50%'; 58 crosshair.style.left = '50%'; 59 return; 60 } 61 62 const hudRect = hud.getBoundingClientRect(); 63 64 if (window.innerHeight <= 600 && window.innerWidth > window.innerHeight) { 65 crosshair.style.top = '50%'; 66 crosshair.style.left = `${hudRect.left / 2}px`; 67 } else { 68 const spaceBelow = window.innerHeight - hudRect.bottom; 69 crosshair.style.top = `${hudRect.bottom + (spaceBelow / 2)}px`; 70 crosshair.style.left = '50%'; 71 } 72} 73window.addEventListener('resize', updateLayout); 74 75// --- AUDIO ENGINE --- 76function initAudio() { 77 if (!audioCtx) { 78 audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 79 } 80 if (audioCtx.state === 'suspended') audioCtx.resume(); 81} 82 83function playSound(type) { 84 if (!audioEnabled) return; 85 initAudio(); 86 87 if (type === 'guess') { 88 const osc = audioCtx.createOscillator(); 89 const gain = audioCtx.createGain(); 90 osc.connect(gain); 91 gain.connect(audioCtx.destination); 92 osc.type = 'sine'; 93 osc.frequency.setValueAtTime(600, audioCtx.currentTime); 94 osc.frequency.exponentialRampToValueAtTime(300, audioCtx.currentTime + 0.1); 95 gain.gain.setValueAtTime(0.2, audioCtx.currentTime); 96 gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1); 97 osc.start(); 98 osc.stop(audioCtx.currentTime + 0.15); 99 } else if (type === 'win') { 100 [523.25, 659.25, 783.99, 1046.50].forEach((freq, i) => { 101 setTimeout(() => { 102 const osc = audioCtx.createOscillator(); 103 const gain = audioCtx.createGain(); 104 osc.connect(gain); 105 gain.connect(audioCtx.destination); 106 osc.type = 'sine'; 107 osc.frequency.value = freq; 108 gain.gain.setValueAtTime(0.2, audioCtx.currentTime); 109 gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4); 110 osc.start(); 111 osc.stop(audioCtx.currentTime + 0.5); 112 }, i * 80); 113 }); 114 } 115} 116 117// --- 2. INITIALIZATION & ROUTING --- 118window.onload = async function() { 119 try { 120 const response = await fetch(`langitude_data.json?v=${new Date().getTime()}`); 121 if (!response.ok) throw new Error("File not found"); 122 wordMapDatabase = await response.json(); 123 } catch (error) { 124 showError("Game data could not be loaded. Please ensure langitude_data.json
124is on the server."); 125 return; 126 } 127 128 const urlParams = new URLSearchParams(window.location.search); 129 const requestedDate = urlParams.get('date'); 130 131 const now = new Date(); 132 const estString = now.toLocaleString("en-US", { timeZone: "America/New_York" }); 133 const estDate = new Date(estString); 134 todayStr = `${estDate.getFullYear()}-${String(estDate.getMonth() + 1).padStart(2, '0')}-${String(estDate.getDate()).padStart(2, '0')}`; 135 136 // --- PREVENT TIME TRAVEL --- 137 if (requestedDate && requestedDate > todayStr) { 138 showError(`Nice try, time traveler! The puzzle for ${requestedDate} isn't available yet. Play today's game or check the archives.`); 139 return; 140 } 141 142 const targetDateStr = requestedDate ? requestedDate : todayStr; 143 144 todaysIndex = wordMapDatabase.findIndex(game => game.date === targetDateStr); 145 146 if (todaysIndex !== -1) { 147 todaysGame = wordMapDatabase[todaysIndex]; 148 } else { 149 showError(`The puzzle for ${targetDateStr} is not available yet. Come back later or play the archives!`); 150 return; 151 } 152 153 unit = localStorage.getItem('wordmap_unit') || 'mi'; 154 timerEnabled = localStorage.getItem('wordmap_timer') === 'true'; 155 startLoc = localStorage.getItem('wordmap_startloc') || 'seattle';
156 mapStyle = localStorage.getItem('wordmap_style') || 'satellite'; 157 hardModeEnabled = localStorage.getItem('wordmap_hardmode') === 'true'; 158 audioEnabled = localStorage.getItem('wordmap_audio') !== 'false'; 159 160 document.getElementById('game-word').innerText = todaysGame.word; 161 document.getElementById('game-definition').innerHTML = todaysGame.definition; 162 document.getElementById('modal-location').innerText = todaysGame.locationName; 163 document.getElementById('modal-etymology').innerHTML = todaysGame.etymology; 164 165 document.getElementById('unit-select').value = unit; 166 document.getElementById('timer-toggle').checked = timerEnabled; 167 document.getElementById('start-loc-select').value = startLoc; 168 169 // Add new styles to dropdown dynamically 170 const styleSelect = document.getElementById('map-style-select'); 171 if (styleSelect.options.length <= 2) { 172 styleSelect.innerHTML = ` 173 <option value="satellite">Satellite</option> 174 <option value="topo">Topographic</option> 175 <option value="osm">Standard Map</option> 176 <option value="light">Light Minimalist</option> 177 <option value="dark">Dark Minimalist</option> 178 `; 179 } 180 styleSelect.value = mapStyle; 181 182 document.getElementById('hardmode-toggle').checked = hardModeEnabled; 183 document.getElementById('audio-toggle').checked = audioEnabled; 184 185 hintsTotal = todaysGame.hints ? todaysGame.hints.length : 0; 186 if (hintsTotal > 0) { 187 document.getElementById('hint-btn').style.display = 'inline-block'; 188 document.getElementById('hint-btn').innerText = `ð¡ Get a hint (${hintsTotal} left)`; 189 } 190 191 initMaps(); 192}; 193 194function showError(message) { 195 document.getElementById('hud-top').style.display = 'none'; 196 document.getElementById('instructions-modal').style.display = 'none'; 197 document.getElementById('error-screen').style.display = 'block'; 198 document.getElementById('error-message').innerText = message; 199} 200 201// --- 3. MAPS & STARTING LOCATION --- 202 203// Records the map's starting location before any guesses are made 204function recordStartLocation() { 205 if (guessesMade === 0) { 206 const center = map.getCenter().wrap(); 207 guessPathCoords = [[center.lat, center.lng]]; 208 } 209} 210 211function setMapStyleLayers() { 212 if (currentBaseLayer) map.removeLayer(currentBaseLayer); 213 if (currentLabelLayer) map.removeLayer(currentLabelLayer); 214 215 const styleData = MAP_STYLES[mapStyle] || MAP_STYLES['satellite']; 216 217 currentBaseLayer = L.tileLayer(styleData.base, { maxZoom: 13 }).addTo(map); 218 219 if (styleData.label && !hardModeEnabled) { 220 currentLabelLayer = L.tileLayer(styleData.label, { maxZoom: 13 }).addTo(map); 221 } 222} 223 224function initMaps() { 225 map = L.map('map2d', { 226 zoomControl: false, 227 minZoom: 2, 228 worldCopyJump: true, 229 attributionControl: false, 230 maxBounds: [[-90, -Infinity], [90, Infinity]], 231 maxBoundsViscosity: 1.0 232 }); 233 234 setMapStyleLayers(); 235 236 if (startLoc === 'random') { 237 let rLat, rLng, distToTarget; 238 do { 239 rLat = (Math.random() - 0.5) * 140; 240 rLng = (Math.random() - 0.5) * 360; 241 distToTarget = turf.distance(turf.point([rLng, rLat]), turf.point([todaysGame.targetLng, todaysGame.targetLat]), {units: 'miles'}); 242 } while (distToTarget < 1000);
243 map.setView([rLat, rLng], 3); 244 recordStartLocation(); 245 } else if (startLoc === 'user' && navigator.geolocation) { 246 navigator.geolocation.getCurrentPosition( 247 (pos) => { map.setView([pos.coords.latitude, pos.coords.longitude], 4); recordStartLocation(); }, 248 () => { map.setView([47.6062, -122.3321], 3); recordStartLocation(); } 249 ); 250 } else { 251 map.setView([47.6062, -122.3321], 3); 252 recordStartLocation(); 253 } 254 255 fetch('geo.json') 256 .then(res => { 257 if (!res.ok) throw new Error("File not found"); 258 return res.json(); 259 }) 260 .then(data => { 261 worldData = data; 262 setupEventListeners(); 263 }) 264 .catch((err) => { 265 console.warn("geo.json failed to load. Falling back to point-to-point."); 266 setupEventListeners(); 267 }); 268} 269 270function getCrosshairLatLng() { 271 const crosshair = document.getElementById('crosshair'); 272 const rect = crosshair.getBoundingClientRect(); 273 const x = rect.left + (rect.width / 2); 274 const y = rect.top + (rect.height / 2); 275 return map.containerPointToLatLng([x, y]).wrap(); 276} 277 278// --- 4. GAMEPLAY --- 279 280// Draws the breadcrumb line connecting the guesses 281function updateGuessPath(latlng) { 282 guessPathCoords.push([latlng.lat, latlng.lng]); 283 284 if (guessPathCoords.length < 2) return; 285 286 if (guessPathLayer) { 287 map.removeLayer(guessPathLayer); 288 } 289 290 guessPathLayer = L.polyline(guessPathCoords, PATH_STYLES[CURRENT_PATH_STYLE]).addTo(map); 291} 292 293function initiateTimer() { 294 if (timerEnabled && !timerStarted && !gameWon) { 295 timerStarted = true; 296 document.getElementById('timer-display').style.display = 'inline-block'; 297 timerInterval = setInterval(() => { 298 secondsElapsed++; 299 document.getElementById('timer-display').innerText = `â±ï¸ ${Math.floor(secondsElapsed / 60)}:${(secondsElapsed % 60).toString().padStart(2, '0')}`; 300 }, 1000); 301 } 302} 303 304function setupEventListeners() { 305 map.once('movestart', initiateTimer); 306 307 document.getElementById('guess-btn').addEventListener('click', () => { 308 initiateTimer(); 309 evaluateGuess(getCrosshairLatLng()); 310 }); 311 312 map.on('move', function() { 313 if (gameWon || !radarActive) return; 314 const targetPoint = getCrosshairLatLng(); 315 const distMiles = turf.distance(turf.point([targetPoint.lng, targetPoint.lat]), turf.point([todaysGame.targetLng, todaysGame.targetLat]), {units: 'miles'}); 316 const displayDist = Math.round(unit === 'mi' ? distMiles : distMiles * 1.60934).toLocaleString(); 317 318 let arrow = ""; 319 if (ENABLE_MERCY_ARROW) { 320 const bearing = turf.bearing(turf.point([targetPoint.lng, targetPoint.lat]), turf.point([todaysGame.targetLng, todaysGame.targetLat])); 321 const arrows = ["â", "â", "â", "â", "â", "â", "â", "â"]; 322 const normalizedBearing = (bearing + 360) % 360; 323 const index = Math.round(normalizedBearing / 45) % 8; 324 arrow = " " + arrows[index]; 325 } 326 327 document.getElementById('last-guess-display').innerText = `You're ${displayDist} ${unit} away.${arrow}`; 328 document.getElementById('last-guess-display').style.color = "#00e5ff"; 329 }); 330 331 if (!localStorage.getItem('wordmap_onboarded')) { 332 document.getElementById('instructions-modal').style.display = 'block'; 333 } else { 334 document.getElementById('hud-top').style.display = 'block'; 335 if (timerEnabled) document.getElementById('timer-display').style.display = 'inline-block'; 336 updateLayout(); 337 } 338} 339 340function evaluateGuess(latlng) { 341 if (gameWon) return; 342 guessesMade++; 343 344 updateGuessPath(latlng); 345 346 const userPoint = turf.point([latlng.lng, latlng.lat]); 347 let isCorrect = false; 348 let finalDistMiles = turf.distance(userPoint, turf.point([todaysGame.targetLng, todaysGame.targetLat]), {units: 'miles'}); 349 350 if (todaysGame.targetType === "country" && worldData) { 351 const targetFeature = worldData.features.find(f => 352 f.id === todaysGame.targetID || 353 (f.properties && ( 354 f.properties.ISO_A3 === todaysGame.targetID || 355 f.properties.iso_a3 === todaysGame.targetID || 356 f.properties.ADM0_A3 === todaysGame.targetID 357 )) 358 ); 359 360 if (targetFeature && turf.booleanPointInPolygon(userPoint, targetFeature)) { 361 isCorrect = true; 362 } 363 } 364 365 const allowedRadius = todaysGame.targetType === "country" ? Math.max(DISTANCE_LOCK, 150) : DISTANCE_LOCK; 366 367 if (finalDistMiles <= allowedRadius) isCorrect = true; 368 369 if (isCorrect) { 370 playSound('win'); 371 const isTrueWin = !radarActive; 372 processStats(isTrueWin); 373 triggerWinSequence(latlng); 374 } else { 375 playSound('guess'); 376 lastMissDistanceMiles = finalDistMiles; 377 378 if (guessesMade >= MAX_GUESSES && !radarActive) { 379 radarActive = true; 380 map.fire('move'); 381 } else { 382 updateMissDisplay(); 383 } 384 } 385} 386 387function updateMissDisplay() { 388 if (lastMissDistanceMiles === null) return; 389 const displayDist = Math.round(unit === 'mi' ? lastMissDistanceMiles : lastMissDistanceMiles * 1.60934).toLocaleStr
389ing(); 390 391 const displayEl = document.getElementById('last-guess-display'); 392 393 if (guessesMade <= MAX_GUESSES) { 394 displayEl.innerText = `Guess ${guessesMade} of ${MAX_GUESSES}: You're ${displayDist} ${unit} away.`; 395 } else { 396 displayEl.innerText = `You're ${displayDist} ${unit} away.`; 397 } 398 399 displayEl.style.backgroundColor = "rgba(255, 68, 68, 0.4)"; 400 displayEl.style.color = "#ffffff"; 401 402 setTimeout(() => { 403 if (!gameWon) { 404 displayEl.style.backgroundColor = "transparent"; 405 displayEl.style.color = "#ffaa00"; 406 } 407 }, 800); 408} 409 410function processStats(isTrueWin) { 411 const urlParams = new URLSearchParams(window.location.search); 412 if (urlParams.get('date') && urlParams.get('date') !== todayStr) return; 413 414 let stats = JSON.parse(localStorage.getItem('wordmap_stats')) || { played: 0, wins: 0, currentStreak: 0, maxStreak: 0, lastDate: null }; 415 416 if (stats.lastDate === todayStr) return populateStatsUI(stats); 417 418 stats.played++; 419 stats.lastDate = todayStr; 420 421 if (isTrueWin) { 422 stats.wins++; 423 stats.currentStreak++; 424 if (stats.currentStreak > stats.maxStreak) stats.maxStreak = stats.currentStreak; 425 } else { 426 stats.currentStreak = 0; 427 } 428 429 localStorage.setItem('wordmap_stats', JSON.stringify(stats)); 430 populateStatsUI(stats); 431} 432 433function populateStatsUI(stats) { 434 const winPct = stats.played > 0 ? Math.round((stats.wins / stats.played) * 100) : 0; 435 document.getElementById('stat-played').innerText = stats.played; 436 document.getElementById('stat-winpct').innerText = `${winPct}%`; 437 document.getElementById('stat-streak').innerText = stats.currentStreak; 438 document.getElementById('stat-maxstreak').innerText = stats.maxStreak; 439} 440 441function triggerWinSequence(finalGuessLatLng) { 442 gameWon = true; 443 clearInterval(timerInterval); 444 445 const displayEl = document.getElementById('last-guess-display'); 446 displayEl.style.backgroundColor = "transparent"; 447 displayEl.innerText = 'LOCKED ON!'; 448 displayEl.style.color = '#00FF00'; 449 450 map.dragging.disable(); 451 map.scrollWheelZoom.disable(); 452 453 const flyTarget = [todaysGame.targetLat, todaysGame.targetLng]; 454 455 const startPt = turf.point([finalGuessLatLng.lng, finalGuessLatLng.lat]); 456 const endPt = turf.point([todaysGame.targetLng, todaysGame.targetLat]); 457 const arc = turf.greatCircle(startPt, endPt); 458 459 const distToTarget = turf.distance(startPt, endPt, {units: 'miles'}); 460 if (distToTarget > 1) { 461 L.geoJSON(arc, { 462 style: { color: '#00FF00', weight: 3, dashArray: '6, 8' } 463 }).addTo(map); 464 } 465 466 if (todaysGame.targetType === "country" && worldData) { 467 const feat = worldData.features.find(f => 468 f.id === todaysGame.targetID || 469 (f.properties && (f.properties.ISO_A3 === todaysGame.targetID || f.properties.iso_a3 === todaysGame.targetID || f.properties.ADM0_A3 === todaysGame.targetID)) 470 ); 471 if (feat) L.geoJSON(feat, { style: { color: "#00FF00", weight: 3, fillOpacity: 0.3 } }).addTo(map); 472 } else { 473 L.circle(flyTarget, { color: '#00FF00', radius: 20000 }).addTo(map); 474 } 475 476 map.flyTo(flyTarget, 5, { duration: 2.5 }); 477 document.getElementById('crosshair').style.opacity = '0'; 478 479 setTimeout(() => { 480 if (document.getElementById('stat-played').innerText === "0") { 481 const stats = JSON.parse(localStorage.getItem('wordmap_stats')) || { played: 0, wins: 0, currentStreak: 0, maxStreak: 0 }; 482 populateStatsUI(stats); 483 } 484 document.getElementById('success-modal').style.display = 'block'; 485 }, 2500); 486} 487 488// --- 5. UI, MENUS & SETTINGS --- 489window.toggleFullscreen = function() { 490 if (!document.fullscreenElement) { 491 document.documentElement.requestFullscreen().catch(err => { 492 console.log(`Error attempting to enable fullscreen: ${err.message}`); 493 }); 494 } else { 495 if (document.exitFullscreen) { 496 document.exitFullscreen(); 497 } 498 } 499}; 500 501window.closeSuccessModal = function() { 502 document.getElementById('success-modal').style.display = 'none'; 503 map.dragging.enable(); 504 map.scrollWheelZoom.enable(); 505 506 document.getElementById('aim-submit-row').style.display = 'none'; 507 document.getElementById('last-guess-display').style.display = 'none'; 508 509 updateLayout(); 510}; 511 512function toggleSidebar() { 513 const sb = document.getElementById('sidebar');
514 sb.style.display = sb.style.display === 'none' ? 'block' : 'none'; 515} 516 517window.openArchive = function(gameType) { 518 toggleSidebar(); 519 520 document.getElementById('success-modal').style.display = 'none'; 521 document.getElementById('settings-modal').style.display = 'none'; 522 523 if (gameType === 'langitude') { 524 const listDiv = document.getElementById('archive-list'); 525 listDiv.innerHTML = ''; 526 527 const pastGames = wordMapDatabase 528 .filter(game => game.date <= todayStr) 529 .sort((a, b) => b.date.localeCompare(a.date)); 530 531 const grouped = {}; 532 pastGames.forEach(game => { 533 const [y, m, d] = game.date.split('-'); 534 const monthKey = `${y}-${m}`; 535 if (!grouped[monthKey]) grouped[monthKey] = []; 536 grouped[monthKey].push(game); 537 }); 538 539 for (const [monthKey, games] of Object.entries(grouped)) { 540 const [y, m] = monthKey.split('-'); 541 const yearNum = parseInt(y); 542 const monthNum = parseInt(m) - 1; 543 const daysInMonth = new Date(yearNum, monthNum + 1, 0).getDate(); 544 const firstDayOfWeek = new Date(yearNum, monthNum, 1).getDay(); 545 546 const monthName = new Date(yearNum, monthNum).toLocaleString('default', { month: 'long', year: 'numeric' }); 547 548 const monthTitle = document.createElement('h3'); 549 monthTitle.innerText = monthName; 550 monthTitle.style.color = '#4CAF50'; 551 monthTitle.style.marginTop = '15px'; 552 monthTitle.style.marginBottom = '5px'; 553 monthTitle.style.borderBottom = '1px solid rgba(255,255,255,0.1)'; 554 listDiv.appendChild(monthTitle); 555 556 const grid = document.createElement('div'); 557 grid.className = 'calendar-grid'; 558 559 const daysOfWeek = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; 560 daysOfWeek.forEach(day => { 561 const el = document.createElement('div'); 562 el.innerText = day; 563 el.className = 'calendar-header'; 564 grid.appendChild(el); 565 }); 566 567 for (let i = 0; i < firstDayOfWeek; i++) { 568 grid.appendChild(document.createElement('div')); 569 } 570 571 const gamesByDay = {}; 572 games.forEach(g => { 573 const d = parseInt(g.date.split('-')[2]); 574 gamesByDay[d] = g; 575 }); 576 577 for (let day = 1; day <= daysInMonth; day++) { 578 const game = gamesByDay[day]; 579 580 if (game && game.date <= todayStr) { 581 const link = document.createElement('a'); 582 link.href = `?date=${game.date}`; 583 link.innerText = day; 584 link.title = game.word; 585 link.className = 'calendar-day active-day'; 586 if (game.date === todayStr) link.classList.add('today-day'); 587 grid.appendChild(link); 588 } else { 589 const emptyDay = document.createElement('div'); 590 emptyDay.innerText = day; 591 emptyDay.className = 'calendar-day disabled-day'; 592 grid.appendChild(emptyDay); 593 } 594 } 595 listDiv.appendChild(grid); 596 } 597 document.getElementById('archive-modal').style.display = 'block'; 598 } 599}; 600 601document.getElementById('hint-btn').addEventListener('click', () => { 602 initiateTimer(); 603 if (hintsUsed < hintsTotal) { 604 document.getElementById('hint-display').style.display = 'block'; 605 document.getElementById('hint-display').innerHTML = todaysGame.hints[hintsUsed++]; 606 607 const hintsLeft = hintsTotal - hintsUsed; 608 const btn = document.getElementById('hint-btn'); 609 if (hintsLeft > 0) { 610 btn.innerText = `ð¡ Get a hint (${hintsLeft} left)`; 611 } else { 612 btn.innerText = `ð¡ No hints left`; 613 btn.disabled = true; 614 btn.classList.add('disabled-btn'); 615 } 616 617 setTimeout(() => {
618 const hud = document.getElementById('hud-top'); 619 hud.scrollTo({ top: hud.scrollHeight, behavior: 'smooth' }); 620 updateLayout(); 621 }, 50); 622 } 623}); 624 625document.getElementById('gear-btn').addEventListener('click', () => { document.getElementById('settings-modal').style.display = 'block'; }); 626 627window.closeSettings = function() { 628 unit = document.getElementById('unit-select').value; 629 startLoc = document.getElementById('start-loc-select').value; 630 631 const newMapStyle = document.getElementById('map-style-select').value; 632 const newHardMode = document.getElementById('hardmode-toggle').checked; 633 audioEnabled = document.getElementById('audio-toggle').checked; 634 635 localStorage.setItem('wordmap_unit', unit); 636 localStorage.setItem('wordmap_startloc', startLoc); 637 localStorage.setItem('wordmap_hardmode', newHardMode); 638 localStorage.setItem('wordmap_audio', audioEnabled); 639 640 if (newMapStyle !== mapStyle || newHardMode !== hardModeEnabled) { 641 mapStyle = newMapStyle; 642 hardModeEnabled = newHardMode; 643 localStorage.setItem('wordmap_style', mapStyle); 644 setMapStyleLayers(); 645 } 646 647 if (!gameWon && lastMissDistanceMiles !== null) { 648 updateMissDisplay(); 649 if (radarActive) map.fire('move'); 650 } 651 652 const prevTimer = timerEnabled; 653 timerEnabled = document.getElementById('timer-toggle').checked; 654 localStorage.setItem('wordmap_timer', timerEnabled); 655 656 if (!timerEnabled) { 657 clearInterval(timerInterval); 658 timerInterval = null; 659 timerStarted = false; 660 document.getElementById('timer-display').style.display = 'none'; 661 } else if (timerEnabled && !prevTimer && !timerStarted) { 662 document.getElementById('timer-display').style.display = 'inline-block'; 663 document.getElementById('timer-display').innerText = "â±ï¸ 0:00"; 664 } 665 666 document.getElementById('settings-modal').style.display = 'none'; 667 668 if (!gameWon && !timerStarted && guessesMade === 0) { 669 if (startLoc === 'random') { 670 let rLat, rLng, distToTarget; 671 do { 672 rLat = (Math.random() - 0.5) * 140; 673 rLng = (Math.random() - 0.5) * 360; 674 distToTarget = turf.distance(turf.point([rLng, rLat]), turf.point([todaysGame.targetLng, todaysGame.targetLat]), {units: 'miles'}); 675 } while (distToTarget < 1000);
676 map.setView([rLat, rLng], 3); 677 recordStartLocation(); 678 } 679 else if (startLoc === 'seattle') { 680 map.setView([47.6062, -122.3321], 3); 681 recordStartLocation(); 682 } 683 else if (startLoc === 'user' && navigator.geolocation) { 684 navigator.geolocation.getCurrentPosition((pos) => { 685 map.setView([pos.coords.latitude, pos.coords.longitude], 4); 686 recordStartLocation(); 687 }); 688 } 689 } 690}; 691 692window.closeInstructions = function() { 693 document.getElementById('instructions-modal').style.display = 'none'; 694 document.getElementById('hud-top').style.display = 'block'; 695 localStorage.setItem('wordmap_onboarded', 'true'); 696 updateLayout(); 697}; 698 699window.copyShareText = function() { 700 const hintStr = hintsTotal > 0 ? `\nð¡ Hints: ${hintsUsed}/${hintsTotal}` : ''; 701 const guessRatio = guessesMade <= MAX_GUESSES && !radarActive ? `${guessesMade}/${MAX_GUESSES}` : `X/${MAX_GUESSES}`; 702 703 const text = `Langitude ${todaysGame.date}\nWord: ${todaysGame.word}\nð¯ Guesses: ${guessRatio}${hintStr}\nwordsmith.org/langitude`; 704 705 navigator.clipboard.writeText(text); 706 document.getElementById('share-btn').innerText = 'Copied!'; 707};
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.