(function(){ const host = document.getElementById("toastHost"); function iconFor(type){ if(type === "success") return "✅"; if(type === "error") return "⛔"; if(type === "info") return "🛰️"; return "🔔"; } window.toast = function(type="info", title="Notice", message="", ttl=3200){ if(!host) return; const el = document.createElement("div"); el.className = `toast toast-${type}`; el.innerHTML = `
${iconFor(type)}
${escapeHtml(title)}
${escapeHtml(message)}
`; const remove = () => { el.classList.add("out"); el.addEventListener("animationend", () => el.remove(), { once:true }); }; el.querySelector(".toast-x").addEventListener("click", remove); host.appendChild(el); if (ttl > 0) setTimeout(remove, ttl); }; function escapeHtml(s){ return String(s ?? "") .replaceAll("&","&") .replaceAll("<","<") .replaceAll(">",">") .replaceAll('"',""") .replaceAll("'","'"); } // Notifications dropdown (tiny JS) const notif = document.getElementById("notifPanel"); window.toggleNotif = function(){ if(!notif) return; const isHidden = notif.hasAttribute("hidden"); if(isHidden) notif.removeAttribute("hidden"); else notif.setAttribute("hidden",""); }; document.addEventListener("click", (e)=>{ if(!notif) return; const btn = e.target.closest(".iconbtn"); const inside = e.target.closest("#notifPanel"); if(inside || btn) return; notif.setAttribute("hidden",""); }); // Settings helpers: perf + alertpulse (no cookies) function setStorage(key, val, remember){ try{ if (remember) { localStorage.setItem(key, val); sessionStorage.removeItem(key); } else { sessionStorage.setItem(key, val); localStorage.removeItem(key); } }catch(e){} } function applyDataset(key, val){ document.documentElement.dataset[key] = val; if (key === "perf") { const label = document.getElementById("perfLabel"); if(label) label.textContent = val.toUpperCase(); } } window.applyPerfFromUI = function(){ const chosen = document.querySelector(".seg-btn.is-selected[data-perf]")?.dataset.perf; const remember = document.getElementById("rememberPerf")?.checked; const val = chosen || "auto"; setStorage("perf", val, remember); applyDataset("perf", val); toast("success","Performance", `Profil: ${val.toUpperCase()}`, 2200); if (val === "low") toast("info","Hint","Low deaktiviert Starfield", 1800); }; window.applyPulseFromUI = function(){ const chosen = document.querySelector(".seg-btn.is-selected[data-alertpulse]")?.dataset.alertpulse; const remember = document.getElementById("rememberPulse")?.checked; const val = chosen || "burst"; setStorage("alertpulse", val, remember); applyDataset("alertpulse", val); toast("success","Alerts", `Pulse: ${val.toUpperCase()}`, 2200); }; // Segment button selection behavior document.addEventListener("click", (e)=>{ const b = e.target.closest(".seg-btn"); if(!b) return; const parent = b.closest(".seg"); if(parent){ parent.querySelectorAll(".seg-btn").forEach(x=>x.classList.remove("is-selected")); b.classList.add("is-selected"); } }); // Pre-select in settings pages document.addEventListener("DOMContentLoaded", ()=>{ const perf = document.documentElement.dataset.perf || "auto"; document.querySelectorAll('.seg-btn[data-perf]').forEach(b=>{ if(b.dataset.perf === perf) b.classList.add("is-selected"); }); const pulse = document.documentElement.dataset.alertpulse || "burst"; document.querySelectorAll('.seg-btn[data-alertpulse]').forEach(b=>{ if(b.dataset.alertpulse === pulse) b.classList.add("is-selected"); }); }); const authView = document.getElementById("authView"); const gameView = document.getElementById("gameView"); let stateTimer = null; const authPanels = { login: document.getElementById("authLogin"), "register-step1": document.getElementById("authRegisterStep1"), "register-step2": document.getElementById("authRegisterStep2"), "register-step3": document.getElementById("authRegisterStep3") }; let racesCache = null; let avatarsCache = null; const regDraft = { race_key: null, avatar_key: null }; function formatNumber(val){ const num = Number.isFinite(val) ? val : 0; return Math.round(num).toLocaleString("de-DE"); } function updateResourceBar(state){ const values = state?.resources || {}; document.querySelectorAll("[data-resource-value]").forEach((valueEl)=>{ const key = valueEl.dataset.resourceValue; if(!key) return; const value = values[key]; if(typeof value !== "number") return; const dot = valueEl.querySelector(".dot"); const display = key === "energy" ? Math.round(value) : Math.floor(value); const prefix = key === "energy" && display >= 0 ? "+" : ""; valueEl.textContent = ""; if(dot) valueEl.appendChild(dot); valueEl.appendChild(document.createTextNode(` ${prefix}${formatNumber(display)}`)); }); } function updateQueuePanel(state){ const slotsEl = document.getElementById("queueSlots"); if(slotsEl){ const slots = Number.isFinite(state?.queue_slots) ? state.queue_slots : null; slotsEl.textContent = slots === null ? "–" : String(slots); } const list = document.getElementById("queueList"); if(!list) return; list.textContent = ""; const jobs = Array.isArray(state?.active_build_jobs) ? state.active_build_jobs : []; if(jobs.length === 0){ const li = document.createElement("li"); li.className = "muted"; li.textContent = "Keine aktiven Baujobs."; list.appendChild(li); return; } jobs.forEach((job)=>{ const li = document.createElement("li"); const slotIndex = Number.isFinite(Number(job?.slot_index)) ? Number(job.slot_index) + 1 : null; const slotLabel = slotIndex ? `Slot ${slotIndex}: ` : ""; const key = job?.building_key ? String(job.building_key) : "Unbekannt"; const finish = job?.finish_at ? ` · fertig ${job.finish_at}` : ""; li.textContent = `${slotLabel}${key}${finish}`; list.appendChild(li); }); } function showAuthView(){ if(authView) authView.removeAttribute("hidden"); if(gameView) gameView.setAttribute("hidden", ""); } function showGameView(){ if(authView) authView.setAttribute("hidden", ""); if(gameView) gameView.removeAttribute("hidden"); } function showAuthPanel(key){ Object.values(authPanels).forEach((panel)=>{ if(panel) panel.setAttribute("hidden", ""); }); const panel = authPanels[key]; if(panel) panel.removeAttribute("hidden"); } function setMessage(el, message){ if(!el) return; el.textContent = message || ""; } async function fetchJson(url, options){ try{ const res = await fetch(url, options); const data = await res.json().catch(()=> ({})); return { ok: res.ok, status: res.status, data }; }catch(e){ return { ok: false, status: 0, data: {} }; } } async function loadRaces(){ if(racesCache) return racesCache; const result = await fetchJson("/api/meta/races"); if(result.ok){ racesCache = result.data.races || []; }else{ racesCache = []; } return racesCache; } async function loadAvatars(){ if(avatarsCache) return avatarsCache; const result = await fetchJson("/api/meta/avatars"); if(result.ok){ avatarsCache = result.data.avatars || []; }else{ avatarsCache = []; } return avatarsCache; } function renderRaces(races){ const list = document.getElementById("raceList"); if(!list) return; list.textContent = ""; if(!Array.isArray(races) || races.length === 0){ const empty = document.createElement("div"); empty.className = "muted"; empty.textContent = "Keine Rassen verfügbar."; list.appendChild(empty); return; } races.forEach((race)=>{ const btn = document.createElement("button"); btn.type = "button"; btn.className = "auth-choice"; btn.dataset.raceKey = race.key; if(regDraft.race_key === race.key) btn.classList.add("is-selected"); const summary = Array.isArray(race.modifier_summary) ? race.modifier_summary : []; btn.innerHTML = `
${escapeHtml(race.name || race.key)}
${escapeHtml(race.description || "")}
${summary.length ? `
${summary.map(s=>escapeHtml(s)).join("
")}
` : ""} `; btn.addEventListener("click", ()=>{ regDraft.race_key = race.key; list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected")); btn.classList.add("is-selected"); }); list.appendChild(btn); }); } function renderAvatars(avatars){ const list = document.getElementById("avatarList"); if(!list) return; list.textContent = ""; if(!Array.isArray(avatars) || avatars.length === 0){ const empty = document.createElement("div"); empty.className = "muted"; empty.textContent = "Keine Avatare verfügbar."; list.appendChild(empty); return; } avatars.forEach((avatar)=>{ const btn = document.createElement("button"); btn.type = "button"; btn.className = "auth-choice"; btn.dataset.avatarKey = avatar.key; if(regDraft.avatar_key === avatar.key) btn.classList.add("is-selected"); btn.innerHTML = ` ${escapeHtml(avatar.label || avatar.key)}
${escapeHtml(avatar.label || avatar.key)}
`; btn.addEventListener("click", ()=>{ regDraft.avatar_key = avatar.key; list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected")); btn.classList.add("is-selected"); }); list.appendChild(btn); }); } function startPolling(){ if(stateTimer) return; stateTimer = setInterval(refreshState, 15000); } function stopPolling(){ if(!stateTimer) return; clearInterval(stateTimer); stateTimer = null; } async function fetchState(){ try{ const res = await fetch("/api/state", { credentials: "same-origin" }); if(res.status === 401) return { status: 401 }; if(!res.ok) return { status: res.status }; const data = await res.json(); return { status: 200, data }; }catch(e){ return { status: 0 }; } } async function refreshState(){ const result = await fetchState(); if(result.status === 200){ showGameView(); updateResourceBar(result.data); updateQueuePanel(result.data); ensureBuildButton(); startPolling(); return; } if(result.status === 401){ showAuthView(); showAuthPanel("login"); stopPolling(); } } function ensureBuildButton(){ const content = document.getElementById("content"); if(!content || document.getElementById("buildOreMine")) return; const wrap = document.createElement("div"); wrap.className = "actions"; const btn = document.createElement("button"); btn.className = "btn btn-primary"; btn.id = "buildOreMine"; btn.type = "button"; btn.textContent = "Erzmine bauen"; btn.addEventListener("click", async ()=>{ try{ const res = await fetch("/api/build/start", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({building_key:"ore_mine", amount:1}) }); const data = await res.json(); if(res.ok){ toast("success","Bau gestartet","Erzmine in Queue gelegt"); updateResourceBar({resources: data.resources}); }else{ toast("error","Bau fehlgeschlagen", data.message || "Aktion nicht möglich"); } }catch(e){ toast("error","Netzwerk","API nicht erreichbar"); } }); wrap.appendChild(btn); content.appendChild(wrap); } document.addEventListener("DOMContentLoaded", ()=>{ document.querySelectorAll("[data-auth-switch]").forEach((btn)=>{ btn.addEventListener("click", async ()=>{ const target = btn.dataset.authSwitch; if(!target) return; showAuthView(); showAuthPanel(target); if(target === "register-step1"){ const races = await loadRaces(); renderRaces(races); } if(target === "register-step2"){ const avatars = await loadAvatars(); renderAvatars(avatars); } }); }); const loginForm = document.getElementById("loginForm"); if(loginForm){ loginForm.addEventListener("submit", async (e)=>{ e.preventDefault(); const identifier = document.getElementById("loginIdentifier")?.value?.trim() || ""; const password = document.getElementById("loginPassword")?.value || ""; const message = document.getElementById("loginMessage"); setMessage(message, ""); const result = await fetchJson("/api/auth/login", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ username_or_email: identifier, password }) }); if(result.ok){ showGameView(); refreshState(); }else{ setMessage(message, result.data?.message || "Login fehlgeschlagen."); } }); } const raceNext = document.getElementById("raceNext"); if(raceNext){ raceNext.addEventListener("click", async ()=>{ const message = document.getElementById("raceMessage"); setMessage(message, ""); if(!regDraft.race_key){ setMessage(message, "Bitte eine Rasse wählen."); return; } const result = await fetchJson("/api/auth/register/step1", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ race_key: regDraft.race_key }) }); if(result.ok){ showAuthPanel("register-step2"); const avatars = await loadAvatars(); renderAvatars(avatars); }else{ setMessage(message, result.data?.message || "Schritt 1 fehlgeschlagen."); } }); } const avatarNext = document.getElementById("avatarNext"); if(avatarNext){ avatarNext.addEventListener("click", async ()=>{ const title = document.getElementById("regTitle")?.value?.trim() || ""; const message = document.getElementById("avatarMessage"); setMessage(message, ""); if(!regDraft.avatar_key){ setMessage(message, "Bitte einen Avatar wählen."); return; } if(title.length < 2){ setMessage(message, "Titel ist zu kurz."); return; } const result = await fetchJson("/api/auth/register/step2", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ avatar_key: regDraft.avatar_key, title }) }); if(result.ok){ showAuthPanel("register-step3"); }else{ setMessage(message, result.data?.message || "Schritt 2 fehlgeschlagen."); } }); } const registerForm = document.getElementById("registerForm"); if(registerForm){ registerForm.addEventListener("submit", async (e)=>{ e.preventDefault(); const username = document.getElementById("regUsername")?.value?.trim() || ""; const email = document.getElementById("regEmail")?.value?.trim() || ""; const password = document.getElementById("regPassword")?.value || ""; const message = document.getElementById("registerMessage"); setMessage(message, ""); const result = await fetchJson("/api/auth/register/step3", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ username, email, password }) }); if(result.ok){ showGameView(); refreshState(); }else{ setMessage(message, result.data?.message || "Registrierung fehlgeschlagen."); } }); } const logoutBtn = document.getElementById("logoutBtn"); if(logoutBtn){ logoutBtn.addEventListener("click", async ()=>{ await fetchJson("/api/auth/logout", { method: "POST" }); stopPolling(); showAuthView(); showAuthPanel("login"); }); } showAuthPanel("login"); refreshState(); }); })();