| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import './styles/main.css'; |
|
|
| import { Scene3D } from './scene/Scene3D.js'; |
| import { TTS } from './audio/TTS.js'; |
| import { AmbientAudio } from './audio/AmbientAudio.js'; |
| import { SoundLibrary } from './audio/SoundLibrary.js'; |
| import { Auth } from './auth/Auth.js'; |
| import * as Store from './game/Store.js'; |
| import { loadFullDictionary } from './game/Dictionary.js'; |
| import { Game } from './game/Game.js'; |
| import { AuthUI } from './ui/AuthUI.js'; |
| import { LeaderboardUI } from './ui/LeaderboardUI.js'; |
| import { GameUI } from './ui/GameUI.js'; |
| import { AdminUI } from './ui/AdminUI.js'; |
|
|
| |
| const bootLoader = { |
| msgEl: () => document.getElementById('bootMsg'), |
| barEl: () => document.getElementById('bootBar'), |
| set(pct, msg) { |
| const m = this.msgEl(); if (m && msg) m.textContent = msg; |
| const b = this.barEl(); if (b) b.style.width = Math.max(0, Math.min(100, pct)) + '%'; |
| }, |
| done() { |
| const el = document.getElementById('bootLoader'); |
| if (!el) return; |
| el.classList.add('gone'); |
| setTimeout(() => el.remove(), 700); |
| }, |
| }; |
|
|
| async function boot() { |
| bootLoader.set(5, 'Initialising…'); |
|
|
| |
| const adminUrl = (() => { |
| const url = new URL(location.href); |
| if (url.searchParams.has('admin')) return true; |
| if (location.hash === '#admin') return true; |
| if (/\/admin\/?$/.test(location.pathname)) return true; |
| return false; |
| })(); |
|
|
| |
| |
| if (adminUrl) { |
| const me = Auth.current(); |
| if (me && !me.isAdmin) await Auth.logout().catch(() => {}); |
| } |
|
|
| |
| const leaderboardUI = new LeaderboardUI(); |
| const authUI = new AuthUI({ onEnter: () => enterApp(), adminOnly: adminUrl }); |
|
|
| |
| |
| const adminUI = new AdminUI({ |
| onSwitchToGame: () => { |
| document.getElementById('appWrap').style.display = ''; |
| document.getElementById('bg3d').style.display = 'block'; |
| |
| const url = new URL(location.href); |
| url.searchParams.delete('admin'); |
| if (url.hash === '#admin') url.hash = ''; |
| history.replaceState(null, '', url); |
| }, |
| }); |
| document.getElementById('adminBtn')?.addEventListener('click', () => openAdminPage()); |
|
|
| |
| |
| |
| |
| |
| |
| function isAdminRoute() { |
| const url = new URL(location.href); |
| if (url.searchParams.has('admin')) return true; |
| if (location.hash === '#admin') return true; |
| if (/\/admin\/?$/.test(location.pathname)) return true; |
| return false; |
| } |
|
|
| function openAdminPage() { |
| if (!Auth.isAdmin()) { |
| alert('Admin access only. Sign in with an admin account.'); |
| return; |
| } |
| document.getElementById('appWrap').style.display = 'none'; |
| document.getElementById('bg3d').style.display = 'none'; |
| adminUI.openAsPage(); |
| } |
|
|
| async function enterApp(asGuest) { |
| try { |
| |
| |
| |
| |
| |
| |
| const me = asGuest ? null : Auth.current(); |
| const ns = (me?.email || 'guest').toLowerCase(); |
| if (Store.getNamespace() !== ns) { |
| await Store.setNamespace(ns); |
| |
| |
| |
| |
| |
| |
| await Store.syncFromRemote(); |
| await game.reloadProgress(); |
| await ui.applySettings(); |
| ui.renderAll(); |
| } |
|
|
| leaderboardUI.refreshUserChip(); |
| leaderboardUI.render(); |
| leaderboardUI.startAutoRefresh(); |
|
|
| const adminBtn = document.getElementById('adminBtn'); |
| if (adminBtn) adminBtn.style.display = Auth.isAdmin() ? 'block' : 'none'; |
|
|
| |
| |
| if (isAdminRoute()) { |
| if (Auth.isAdmin()) openAdminPage(); |
| else alert('Admin access only. Sign in with an admin account.'); |
| } |
| } catch (e) { console.warn('enterApp non-fatal:', e); } |
| } |
|
|
| |
| function showStorageWarning(msg) { |
| if (!msg || document.getElementById('storageWarning')) return; |
| const el = document.createElement('div'); |
| el.id = 'storageWarning'; |
| el.textContent = '⚠ ' + msg; |
| el.style.cssText = ` |
| position:fixed;top:0;left:0;right:0;z-index:99999; |
| background:#FF6B6B;color:#fff;padding:8px 14px; |
| font:600 13px/1.3 system-ui,sans-serif;text-align:center; |
| box-shadow:0 2px 6px rgba(0,0,0,.3);cursor:pointer; |
| `; |
| el.title = 'Click to dismiss'; |
| el.addEventListener('click', () => el.remove()); |
| document.body.appendChild(el); |
| } |
|
|
| |
| bootLoader.set(15, 'Building the 3D world…'); |
| let scene3d; |
| try { |
| scene3d = new Scene3D(document.getElementById('bg3d')); |
| } catch (e) { |
| console.error('3D scene failed to initialize:', e); |
| |
| scene3d = { |
| setIntensity() {}, |
| setAmbient(on) { document.getElementById('bg3d').style.display = on ? 'block' : 'none'; }, |
| celebrate() {}, |
| }; |
| } |
|
|
| |
| bootLoader.set(25, 'Opening local database…'); |
| await Store.open(); |
|
|
| |
| |
| |
| { |
| const me = Auth.current(); |
| if (me?.email) await Store.setNamespace(me.email.toLowerCase()); |
| } |
|
|
| |
| |
| showStorageWarning(Store.warning()); |
|
|
| |
| const tts = new TTS({ |
| audioEl: document.getElementById('audio'), |
| speakerEl: document.getElementById('speaker'), |
| }); |
|
|
| |
| |
| const ambient = new AmbientAudio(); |
| if (scene3d?.gameClock) ambient.setClock(scene3d.gameClock); |
|
|
| |
| |
| const soundLib = new SoundLibrary(); |
| if (scene3d?.gameClock) soundLib.setClock(scene3d.gameClock); |
|
|
| |
| const _startAmbient = () => { |
| ambient.start(); |
| soundLib.start(); |
| window.removeEventListener('pointerdown', _startAmbient); |
| window.removeEventListener('keydown', _startAmbient); |
| }; |
| window.addEventListener('pointerdown', _startAmbient); |
| window.addEventListener('keydown', _startAmbient); |
| |
| window.__ambient = ambient; |
| window.__sounds = soundLib; |
|
|
| |
| const progress = (m) => { |
| const el = document.getElementById('bootStatus'); if (el) el.textContent = m; |
| |
| const pct = m.includes('cached') ? 80 : |
| m.includes('common-word') ? 40 : |
| m.includes('full English') ? 55 : |
| m.includes('Merging') ? 75 : |
| m.includes('Caching') ? 82 : 50; |
| bootLoader.set(pct, m); |
| }; |
| let dict; |
| try { |
| bootLoader.set(35, 'Loading dictionary…'); |
| dict = await loadFullDictionary({ |
| getCache: Store.getCache, |
| setCache: Store.setCache, |
| progress, |
| }); |
| } catch (e) { |
| bootLoader.set(100, '⚠ Failed to load dictionary — check your internet.'); |
| progress('⚠ Failed to load dictionary — check your internet.'); |
| return; |
| } |
|
|
| |
| bootLoader.set(90, 'Preparing your progress…'); |
| const game = new Game(dict); |
| |
| |
| |
| await Store.syncFromRemote(); |
| await game.loadProgress(); |
|
|
| const ui = new GameUI({ game, tts, scene3d, leaderboardUI }); |
| await ui.applySettings(); |
| ui.renderAll(); |
| ui.finishBoot(`✓ ${dict.set.size.toLocaleString()} English words ready · ${dict.lengths().length} grades`); |
|
|
| |
| bootLoader.set(100, 'Ready.'); |
| if (Auth.session()) { |
| authUI.hide(); |
| enterApp(); |
| } else { |
| authUI.show(); |
| } |
| bootLoader.done(); |
|
|
| |
| window.addEventListener('resize', () => { }); |
| } |
|
|
| boot().catch((e) => { |
| console.error(e); |
| const el = document.getElementById('bootStatus'); |
| if (el) el.innerHTML = `<span style="color:#FF6B6B">Error: ${e.message}</span>`; |
| }); |
|
|