/* OGame Galaxy Viewer v2 — logica frontend */ "use strict"; const $ = (id) => document.getElementById(id); const state = { galaxy: 1, system: 1, maxGalaxy: 0, maxSystem: 0, boot: null }; /* (utility, avatar e stelle ora in common.js) */ /* ---------------- bootstrap ---------------- */ async function loadBootstrap() { try { const b = await api("/api/bootstrap"); state.boot = b; state.maxGalaxy = b.server.galaxies; state.maxSystem = b.server.systems; $("universe-name").textContent = b.server.name; $("universe-sub").textContent = `[s${b.server.number}-${b.server.community}] · velocità ${b.server.speed}x · v${b.server.version}`; $("max-galaxy").textContent = b.server.galaxies; $("max-system").textContent = b.server.systems; $("meta-data-age").textContent = b.fetched.players ? `Dati: ${b.fetched.players} UTC` : ""; $("meta-reports").innerHTML = `Spie archiviate: ${b.reportStats.count} su ${b.reportStats.planets} pianeti`; } catch (e) { setMsg("err", "Impossibile contattare il server: " + e.message); } } /* ---------------- galassia ---------------- */ async function loadGalaxy() { $("nav-status").textContent = `Caricamento G${state.galaxy}:S${state.system}…`; try { const data = await api(`/api/galaxy?g=${state.galaxy}&s=${state.system}`); renderGalaxy(data); $("nav-status").textContent = ""; $("inp-galaxy").value = state.galaxy; $("inp-system").value = state.system; } catch (e) { setMsg("err", e.message); $("nav-status").textContent = ""; } } const stClass = (s) => "st-" + (s || ""); function renderGalaxy(data) { const body = $("galaxy-body"); body.innerHTML = ""; let any = false; for (const cell of data.cells) { const tr = document.createElement("tr"); let td = document.createElement("td"); td.className = "c-pos"; td.textContent = cell.pos; tr.appendChild(td); td = document.createElement("td"); if (cell.planet) { any = true; const mine = cell.planet.mine ? " planet-mine" : ""; const ro = cell.planet.report_only; const roTag = ro ? ` da rapporto` : ""; const ptitle = ro ? "Pianeta noto solo da un rapporto di spionaggio" : "Apri dettagli e rapporti"; td.innerHTML = `
` + `${artSVG(cell.planet.coords, 26)}` + `${esc(cell.planet.name)}
` + `${esc(cell.planet.coords)}${roTag}
`; td.querySelector(".pcell").onclick = () => openPlanet(cell.pos); } tr.appendChild(td); td = document.createElement("td"); if (cell.moon) { td.innerHTML = `${artSVG(cell.planet.coords, 22, true)}` + `${Number(cell.moon.size).toLocaleString("it")}`; td.querySelector(".mcell").onclick = () => openPlanet(cell.pos, "moon"); } tr.appendChild(td); td = document.createElement("td"); if (cell.planet && cell.player) { const p = cell.player; const g = p.rank_total ?? "–"; const m = p.rank_military ?? "–"; td.innerHTML = `${esc(p.name)}` + `` + `${icon("trophy", 11)} ${g}${icon("swords", 11)} ${m}`; const pn = td.querySelector(".player-link"); if (pn) pn.onclick = () => openPlayer(p.id); } tr.appendChild(td); td = document.createElement("td"); if (cell.player && cell.player.alliance_tag) { td.innerHTML = `[${esc(cell.player.alliance_tag)}]`; } tr.appendChild(td); td = document.createElement("td"); td.className = "c-rep"; if (cell.reports && cell.reports.length) { const n = cell.reports.length; const hasMoon = cell.reports.some((r) => r.moon); td.innerHTML = `${icon("badge_spy", 12)} ${n}`; td.querySelector(".rep-badge").onclick = () => openPlanet(cell.pos); } tr.appendChild(td); body.appendChild(tr); } $("empty-state").classList.toggle("hidden", any); } /* ---------------- popup: pianeta ---------------- */ function popupHead(avatar, title, sub) { $("popup-avatar").innerHTML = avatar; $("popup-title").textContent = title; $("popup-sub").textContent = sub; } async function openPlanet(pos, focusMoon) { return openPlanetAt(`${state.galaxy}:${state.system}:${pos}`, focusMoon); } // Apre il pianeta date le coordinate `g:s:p` (può essere in un altro sistema, // senza spostare la vista della galassia). async function openPlanetAt(coords, focusMoon) { const [g, s, p] = String(coords).split(":").map(Number); _lastPlanetCoords = `${g}:${s}:${p}`; try { const data = await api(`/api/planet?g=${g}&s=${s}&p=${p}`); renderPlanetPopup(data, focusMoon); $("overlay").classList.remove("hidden"); } catch (e) { setMsg("err", e.message); } } // Apre la finestra giocatore: tutti i pianeti + tutti i rapporti. async function openPlayer(pid) { try { const d = await api(`/api/player?id=${encodeURIComponent(pid)}`); renderPlayerPopup(d); $("overlay").classList.remove("hidden"); } catch (e) { setMsg("err", e.message); } } const STATUS_LABEL = { "": "attivo", i: "inattivo", v: "in vacanza", vi: "vacanza + inattivo", a: "staff" }; const CAT_ICON = { buildings: "cat_buildings", lfbuildings: "cat_lf", research: "cat_research", ships: "cat_ships", defense: "cat_defense", }; const ITEM_ICON = { buildings: { 1: "b_1", 2: "b_2", 3: "b_3", 4: "b_4", 12: "b_12", 14: "b_14", 15: "b_15", 21: "b_21", 22: "b_22", 23: "b_23", 24: "b_24", 31: "b_31", 33: "b_33", 34: "b_34", 36: "b_36", 41: "b_41", 42: "b_42", 43: "b_43", 44: "b_44" }, research: { 106: "r_106", 108: "r_108", 109: "r_109", 110: "r_110", 111: "r_111", 113: "r_113", 114: "r_114", 115: "r_115", 117: "r_117", 118: "r_118", 120: "r_120", 121: "r_121", 122: "r_122", 123: "r_123", 124: "r_124", 199: "r_199" }, ships: { 202: "ship_202", 203: "ship_203", 204: "ship_204", 205: "ship_205", 206: "ship_206", 207: "ship_207", 208: "ship_208", 209: "ship_209", 210: "ship_210", 211: "ship_211", 212: "ship_212", 213: "ship_213", 214: "ship_214", 215: "ship_215", 217: "ship_217", 218: "ship_218", 219: "ship_219" }, defense: { 401: "d_401", 402: "d_402", 403: "d_403", 404: "d_404", 405: "d_405", 406: "d_406", 407: "d_407", 408: "d_408", 502: "d_502", 503: "d_503" }, lfbuildings: { 11101: "lf_11101", 11102: "lf_11102" }, }; function itemIconName(cat, id) { const t = ITEM_ICON[cat] || {}; return t[id] || CAT_ICON[cat] || "cat_ships"; } function renderPlanetPopup(data, focusMoon) { const pl = data.planet, player = data.player; _lastPlanet = data; _lastPlayer = null; const mine = player.id === String(state.boot.myPlayer.id) ? " (tu)" : ""; const coords = pl.coords; const hasMoon = !!(pl.moon); popupHead(artSVG(coords, 52), `${pl.name} [${coords}]`, "Pianeta · clicca un rapporto per i dettagli"); let h = ""; h += `
`; h += `Giocatore ` + `${esc(player.name)}${mine}`; h += `Stato ${STATUS_LABEL[player.status] || "—"}`; if (player.rank_total || player.rank_military) { h += `Classifica ` + `${icon("trophy", 13)} #${player.rank_total ?? "—"} · ${icon("swords", 13)} #${player.rank_military ?? "—"}`; } if (player.alliance_tag) { h += `Alleanza ` + `[${esc(player.alliance_tag)}] ${esc(player.alliance_name || "")}`; } if (hasMoon) { h += `Luna ` + `${esc(pl.moon.name)} · ${Number(pl.moon.size).toLocaleString("it")} km`; } h += `Pianeti ${1 + (data.other_planets || []).length}`; if (pl.report_only) { h += `Origine solo da rapporto`; } h += `
`; if (data.other_planets && data.other_planets.length) { h += `

Altri pianeti di ${esc(player.name)}

`; h += data.other_planets.map((o) => `${artSVG(o.coords, 17)}` + `${esc(o.name)} [${esc(o.coords)}]${o.moon ? " " + artSVG(o.coords, 13, true) : ""}`).join(""); h += `
`; } const rpt = (list, moon) => `

${moon ? "Spionaggi sulla luna" : "Spionaggi sul pianeta"}` + ` ${list.length}

` + (list.length ? `
${list.map(rptCard).join("")}
` : `
${moon ? "La luna non è mai stata spiata." : "Nessun rapporto salvato. Spia in gioco e incolla l'API string qui sopra."}
`) + `
`; // sezione pianeta: sempre visibile (lista o invito a importare) h += rpt(data.reports, false); // sezione luna: solo se esiste una luna o ci sono spionaggi sulla luna if (hasMoon || data.moon_reports.length) h += rpt(data.moon_reports, true); $("popup-body").innerHTML = h; $("popup-body").querySelectorAll(".chip-other").forEach((c) => { c.onclick = () => { closePopup(); goToCoords(c.dataset.go); }; }); $("popup-body").querySelectorAll(".rpt-card").forEach((c) => { c.onclick = () => openReport(c.dataset.sr); }); if (focusMoon === "moon") { const secs = $("popup-body").querySelectorAll(".sec h4"); for (const s of secs) if (s.textContent.includes("luna")) { s.scrollIntoView({ block: "start" }); break; } } } /* ---------------- popup: giocatore ---------------- */ function renderPlayerPopup(d) { const player = d.player, planets = d.planets || [], reports = d.reports || []; _lastPlayer = d; _lastPlanet = null; const mine = String(player.id) === String(state.boot.myPlayer.id) ? " (tu)" : ""; const avatar = d.avatar || (planets[0] && planets[0].coords) || (player.name + player.id); popupHead(artSVG(avatar, 52), player.name + mine, `${planets.length} pianeti · ${reports.length} rapporti di spionaggio`); let h = ""; h += `
`; h += `Stato ` + `${STATUS_LABEL[player.status] || "—"}`; if (player.rank_total || player.rank_military) { h += `Classifica ` + `${icon("trophy", 13)} #${player.rank_total ?? "—"} · ${icon("swords", 13)} #${player.rank_military ?? "—"}`; } if (player.alliance_tag) { h += `Alleanza ` + `[${esc(player.alliance_tag)}] ${esc(player.alliance_name || "")}`; } h += `Spie ${reports.length}`; h += `
`; // pianeti del giocatore h += `

Pianeti ${planets.length}

`; h += `
` + planets.map((o) => `${artSVG(o.coords, 17)}` + `${esc(o.name)} [${esc(o.coords)}]${o.moon ? " " + artSVG(o.coords, 13, true) : ""}` ).join("") + `
`; // tutti i rapporti del giocatore (pianeti e lune) h += `

Rapporti di spionaggio ${reports.length}

` + (reports.length ? `
${reports.map(rptCard).join("")}
` : `
Nessun rapporto salvato su questo giocatore.
`) + `
`; $("popup-body").innerHTML = h; $("popup-body").querySelectorAll(".chip-other").forEach((c) => { c.onclick = () => openPlanetAt(c.dataset.coords); }); $("popup-body").querySelectorAll(".rpt-card").forEach((c) => { c.onclick = () => openReport(c.dataset.sr); }); } /* ---------------- card rapporto ---------------- */ function actClass(a) { if (a === null || a === undefined) return "act-none"; if (a <= 0) return "act-0"; if (a <= 15) return "act-15"; if (a <= 30) return "act-30"; return "act-60"; } function actLabel(a) { if (a === null || a === undefined) return "attività sconosciuta"; if (a <= 0) return "inattivo ≥7gg"; return `attivo ≤ ${a} min`; } function fmtEv(evt) { if (!evt) return ""; const d = new Date(evt); return isNaN(d) ? evt : d.toLocaleString("it-IT", { dateStyle: "medium", timeStyle: "short" }); } function rptCard(rec) { const c = rec.categories || {}; const loot = rec.loot_percentage !== null && rec.loot_percentage !== undefined; const parts = []; if (c.ships) parts.push(`${icon("cat_ships", 13)} ${c.ships}`); if (c.defense) parts.push(`${icon("cat_defense", 13)} ${c.defense}`); if (c.buildings) parts.push(`${icon("cat_buildings", 13)} ${c.buildings}`); if (c.research) parts.push(`${icon("cat_research", 13)} ${c.research}`); return `
` + `
` + `${fmtEv(rec.event_time)}` + `${rec.moon ? "luna" : "pianeta"}
` + `
${esc(rec.defender_planet_name || "?")}` + (rec.coords ? ` [${esc(rec.coords)}]` : "") + `
da ${esc(rec.attacker_name || "?")}` + (loot ? ` · bottino ${rec.loot_percentage}%` : "") + `
` + `
${parts.map((p) => `${p}`).join("") || "—"}` + `Apri ›
`; } /* ---------------- dettaglio rapporto ---------------- */ let _lastPlanet = null; let _lastPlanetCoords = null; let _lastPlayer = null; async function openReport(srId) { try { const d = await api(`/api/reports/${srId}`); renderReportDetail(d.meta, d.data); } catch (e) { setMsg("err", e.message); } } async function deleteReport(srId) { if (!confirm("Eliminare definitivamente questo rapporto di spionaggio?")) return; try { await api(`/api/reports/${srId}`, { method: "DELETE" }); if (_lastPlanetCoords) { await openPlanetAt(_lastPlanetCoords); // ricarica il pianeta: lista aggiornata } else if (_lastPlayer && _lastPlayer.player) { await openPlayer(_lastPlayer.player.id); // ricarica il giocatore: lista aggiornata } else { closePopup(); } loadGalaxy(); // aggiorna i badge "spie" nella galassia } catch (e) { setMsg("err", "Eliminazione non riuscita: " + e.message); } } function renderReportDetail(meta, rep) { const gen = rep.generic || {}, det = rep.details || {}, res = det.resources || {}; const coords = meta.coords || gen.defender_planet_coordinates || ""; popupHead(artSVG(coords, 52), `Spionaggio — ${meta.defender_planet_name || "?"}`, `${coords}${meta.planet_type === 3 ? " (luna)" : ""} · ${fmtEv(meta.event_time)}`); let h = ""; h += `
` + `` + `` + `
`; h += `
`; h += `Difensore ${esc(meta.defender_name || "?")}`; h += `Attaccante ${esc(gen.attacker_name || "?")}`; h += `Attività ${actLabel(meta.activity)}`; if (meta.loot_percentage !== null && meta.loot_percentage !== undefined) h += `Bottino ${meta.loot_percentage}%`; if (gen.spy_fail_chance !== undefined) h += `Rischio fall. ${gen.spy_fail_chance}%`; if (gen.debris_metal || gen.debris_crystal) h += `Campo detriti ${icon("extra_debris", 13)} ${num(gen.debris_metal)} / ${num(gen.debris_crystal)}`; h += `
`; const rt = (lab, ico, val, cls) => `
${ico}` + `
${lab}
${val}
`; h += `
`; h += rt("Metallo", icon("res_metal", 20), num(res.metal), "res-metal"); h += rt("Cristallo", icon("res_crystal", 20), num(res.crystal), "res-crystal"); h += rt("Deuterio", icon("res_deuterium", 20), num(res.deuterium), "res-deuterium"); if (res.population) h += rt("Popolazione", icon("res_pop", 20), num(res.population), "res-pop"); if (res.food) h += rt("Cibo", icon("res_food", 20), num(res.food), "res-food"); h += `
`; const failed = Object.entries({ "edifici": gen.failed_buildings, "ricerca": gen.failed_research, "navi": gen.failed_ships, "difese": gen.failed_defense, }).filter(([, v]) => v).map(([k]) => k); if (failed.length) h += `
${icon("ui_warn", 14)} Spionaggio fallito su: ${failed.join(", ")} — dati incompleti.
`; h += techSection("Edifici", "buildings", det.buildings, (b) => [b.building_type, `L. ${b.level}`], "liv."); h += techSection("Edifici Lifeform", "lfbuildings", det.lfbuildings, (b) => [b.lfbuilding_type, `L. ${b.level}`], "liv."); h += techSection("Ricerca", "research", det.research, (b) => [b.research_type, `L. ${b.level}`], "liv."); h += techSection("Navi", "ships", det.ships, (b) => [b.ship_type, num(b.count)], "unità"); h += techSection("Difese", "defense", det.defense, (b) => [b.defense_type, num(b.count)], "unità"); $("popup-body").innerHTML = h; $("btn-back").onclick = () => { if (_lastPlanet) renderPlanetPopup(_lastPlanet); else if (_lastPlayer) renderPlayerPopup(_lastPlayer); else closePopup(); }; $("btn-del").onclick = () => deleteReport(meta.sr_id); } function techSection(title, cat, rows, get, valLabel) { if (!rows || !rows.length) return ""; const cards = rows.map((it) => { const [tid, val] = get(it); return `
` + `${icon(itemIconName(cat, tid), 19)}` + `${esc(itemName(cat, tid))}` + `${val}
`; }).join(""); return `

${title} ${rows.length}

` + `
${cards}
`; } /* ---------------- nomi e icone elementi ---------------- */ const NAMES = { buildings: { 1: "Miniera di metallo", 2: "Miniera di cristallo", 3: "Sintetizzatore di deuterio", 4: "Centrale solare", 12: "Centrale a fusione", 14: "Fabbrica di robot", 15: "Fabbrica di naniti", 21: "Cantiere spaziale", 22: "Deposito di metallo", 23: "Deposito di cristallo", 24: "Serbatoio di deuterio", 31: "Laboratorio di ricerca", 33: "Terraformer", 34: "Deposito dell'alleanza", 36: "Dock di riparazione", 41: "Base lunare", 42: "Falanx sensoriale", 43: "Portale quantico", 44: "Silo missilistico", }, research: { 106: "Tecnologia spionaggio", 108: "Tecnologia informatica", 109: "Tecnologia delle armi", 110: "Tecnologia scudi", 111: "Tecnologia corazzatura", 113: "Tecnologia energetica", 114: "Tecnologia iperspazio", 115: "Motore a combustione", 117: "Motore a impulso", 118: "Motore iperspaziale", 120: "Tecnologia laser", 121: "Tecnologia ionica", 122: "Tecnologia al plasma", 123: "Rete di ricerca intergalattica", 124: "Astrofisica", 199: "Tecnologia gravitoni", }, ships: { 202: "Cargo leggero", 203: "Cargo pesante", 204: "Caccia leggero", 205: "Caccia pesante", 206: "Incrociatore", 207: "Nave da battaglia", 208: "Nave colonizzatrice", 209: "Riciclatore", 210: "Sonda di spionaggio", 211: "Bombardiere", 212: "Satellite solare", 213: "Corazzata", 214: "Morte Nera", 215: "Incrociatore da battaglia", 217: "Crawler", 218: "Reaper", 219: "Pathfinder", }, defense: { 401: "Lanciamissili", 402: "Laser leggero", 403: "Laser pesante", 404: "Cannone Gauss", 405: "Cannone ionico", 406: "Cannone al plasma", 407: "Cupola piccola", 408: "Cupola grande", 502: "Missili anti-balistici", 503: "Missili interplanetari", }, lfbuildings: { 11101: "Campo di coltivazione", 11102: "Centro di ricerca" }, }; function itemName(cat, id) { const t = NAMES[cat] || {}; return t[id] || (cat === "lfbuildings" ? "Edificio Lifeform #" + id : cat + " #" + id); } /* ---------------- navigazione ---------------- */ function setSystem(s) { state.system = Math.max(1, Math.min(state.maxSystem, s)); loadGalaxy(); } function setGalaxy(g) { state.galaxy = Math.max(1, Math.min(state.maxGalaxy, g)); loadGalaxy(); } function closePopup() { $("overlay").classList.add("hidden"); _lastPlanet = null; _lastPlayer = null; } function goToCoords(c) { const [g, s] = c.split(":").map(Number); closePopup(); state.galaxy = Math.max(1, Math.min(state.maxGalaxy, g || 1)); state.system = Math.max(1, Math.min(state.maxSystem, s || 1)); loadGalaxy(); } /* ---------------- messaggi / import ---------------- */ function setMsg(kind, text, ms = 6000) { const el = $("import-msg"); el.className = kind; el.textContent = text; if (ms) setTimeout(() => { el.textContent = ""; }, ms); } async function importReport() { const token = $("inp-token").value.trim(); if (!token) { setMsg("err", "Incolla prima l'API string."); return; } const btn = $("btn-import"); btn.disabled = true; setMsg("ok", "Recupero del rapporto…"); try { const res = await api("/api/reports", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), }); $("inp-token").value = ""; setMsg("ok", res.created ? `Importato: ${res.report.defender_name} — ${res.coords}${res.planet_type === 3 ? " (luna)" : ""}` : `Rapporto già in archivio (${res.coords}).`); loadGalaxy(); } catch (e) { setMsg("err", e.message); } finally { btn.disabled = false; } } /* ---------------- eventi ---------------- */ $("btn-import").onclick = importReport; $("inp-token").addEventListener("keydown", (e) => { if (e.key === "Enter") importReport(); }); $("btn-prev-sys").onclick = () => setSystem(state.system - 1); $("btn-next-sys").onclick = () => setSystem(state.system + 1); $("btn-refresh").onclick = () => loadGalaxy(); $("inp-system").addEventListener("change", (e) => setSystem(parseInt(e.target.value, 10) || 1)); $("inp-galaxy").addEventListener("change", (e) => setGalaxy(parseInt(e.target.value, 10) || 1)); $("popup-close").onclick = closePopup; $("overlay").addEventListener("click", (e) => { if (e.target === $("overlay")) closePopup(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !$("overlay").classList.contains("hidden")) closePopup(); }); /* ---------------- init ---------------- */ initStars(); const il = document.getElementById("import-label"); if (il) il.innerHTML = icon("ui_import", 15) + ' Nuovo rapporto'; const tli = document.querySelector(".tl-ico"); if (tli) tli.innerHTML = icon("trophy", 14); const pcl = document.getElementById("popup-close"); if (pcl) pcl.innerHTML = icon("ui_close", 16); const _qs = new URLSearchParams(location.search); const _g = parseInt(_qs.get("g") || "", 10); const _s = parseInt(_qs.get("s") || "", 10); (async () => { await loadBootstrap(); if (_g) state.galaxy = Math.max(1, Math.min(state.maxGalaxy, _g)); if (_s) state.system = Math.max(1, Math.min(state.maxSystem, _s)); await loadGalaxy(); })();