501 lines
17 KiB
JavaScript
501 lines
17 KiB
JavaScript
(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 = `
|
||
<div class="toast-icon">${iconFor(type)}</div>
|
||
<div class="toast-body">
|
||
<div class="toast-title">${escapeHtml(title)}</div>
|
||
<div class="toast-msg">${escapeHtml(message)}</div>
|
||
</div>
|
||
<button class="toast-x" aria-label="Close">✕</button>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="auth-choice-title">${escapeHtml(race.name || race.key)}</div>
|
||
<div class="auth-choice-meta">${escapeHtml(race.description || "")}</div>
|
||
${summary.length ? `<div class="auth-choice-meta">${summary.map(s=>escapeHtml(s)).join("<br>")}</div>` : ""}
|
||
`;
|
||
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 = `
|
||
<img class="auth-avatar" src="${escapeHtml(avatar.image || "")}" alt="${escapeHtml(avatar.label || avatar.key)}">
|
||
<div class="auth-choice-title">${escapeHtml(avatar.label || avatar.key)}</div>
|
||
`;
|
||
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();
|
||
});
|
||
})();
|