primo commit

This commit is contained in:
2026-09-23 18:59:27 +02:00
parent 029e9f67fb
commit bd900099a0
451 changed files with 143273 additions and 2 deletions
+548
View File
@@ -0,0 +1,548 @@
/* 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>${b.reportStats.count}</b> 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
? ` <span class="ro-tag" title="Noto solo da un rapporto di spionaggio">da rapporto</span>` : "";
const ptitle = ro ? "Pianeta noto solo da un rapporto di spionaggio" : "Apri dettagli e rapporti";
td.innerHTML =
`<div class="pcell${mine}" title="${ptitle}">` +
`<span class="p-avatar">${artSVG(cell.planet.coords, 26)}</span>` +
`<span><span class="pname">${esc(cell.planet.name)}</span><br>` +
`<span class="pcoords">${esc(cell.planet.coords)}</span>${roTag}</span></div>`;
td.querySelector(".pcell").onclick = () => openPlanet(cell.pos);
}
tr.appendChild(td);
td = document.createElement("td");
if (cell.moon) {
td.innerHTML = `<span class="mcell">${artSVG(cell.planet.coords, 22, true)}` +
`<span>${Number(cell.moon.size).toLocaleString("it")}</span></span>`;
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 =
`<span class="pname-status player-link ${stClass(p.status)}" ` +
`title="Tutti i pianeti e i rapporti di questo giocatore">${esc(p.name)}</span>` +
`<span class="pranks" title="Posizioni in classifica — Generale: #${g} · Armamenti: #${m}">` +
`<span class="rk">${icon("trophy", 11)} ${g}</span><span class="rk">${icon("swords", 11)} ${m}</span></span>`;
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 = `<span class="ally-tag">[${esc(cell.player.alliance_tag)}]</span>`;
}
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 = `<span class="rep-badge${hasMoon ? " rep-moon" : ""}" ` +
`title="${n} rapporto/i di spionaggio">${icon("badge_spy", 12)} ${n}</span>`;
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 += `<div class="kv">`;
h += `<span class="chip"><span class="k">Giocatore</span> ` +
`<span class="v ${stClass(player.status)}">${esc(player.name)}</span>${mine}</span>`;
h += `<span class="chip"><span class="k">Stato</span> <span class="v">${STATUS_LABEL[player.status] || "—"}</span></span>`;
if (player.rank_total || player.rank_military) {
h += `<span class="chip"><span class="k">Classifica</span> <span class="v">` +
`${icon("trophy", 13)} #${player.rank_total ?? "—"} · ${icon("swords", 13)} #${player.rank_military ?? "—"}</span></span>`;
}
if (player.alliance_tag) {
h += `<span class="chip"><span class="k">Alleanza</span> ` +
`<span class="v">[${esc(player.alliance_tag)}]</span> ${esc(player.alliance_name || "")}</span>`;
}
if (hasMoon) {
h += `<span class="chip"><span class="k">Luna</span> <span class="v">` +
`${esc(pl.moon.name)} · ${Number(pl.moon.size).toLocaleString("it")} km</span></span>`;
}
h += `<span class="chip"><span class="k">Pianeti</span> <span class="v">${1 + (data.other_planets || []).length}</span></span>`;
if (pl.report_only) {
h += `<span class="chip"><span class="k">Origine</span> <span class="v">solo da rapporto</span></span>`;
}
h += `</div>`;
if (data.other_planets && data.other_planets.length) {
h += `<div class="sec"><h4>Altri pianeti di ${esc(player.name)}</h4><div class="kv">`;
h += data.other_planets.map((o) =>
`<span class="chip chip-other" data-go="${esc(o.coords)}">${artSVG(o.coords, 17)}` +
`${esc(o.name)} [${esc(o.coords)}]${o.moon ? " " + artSVG(o.coords, 13, true) : ""}</span>`).join("");
h += `</div></div>`;
}
const rpt = (list, moon) =>
`<div class="sec"><h4>${moon ? "Spionaggi sulla luna" : "Spionaggi sul pianeta"}` +
` <span class="count-pill">${list.length}</span></h4>` +
(list.length
? `<div class="rpt-grid">${list.map(rptCard).join("")}</div>`
: `<div class="empty-note">${moon
? "La luna non è mai stata spiata."
: "Nessun rapporto salvato. Spia in gioco e incolla l'API string qui sopra."}</div>`) +
`</div>`;
// 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 += `<div class="kv">`;
h += `<span class="chip"><span class="k">Stato</span> <span class="v ${stClass(player.status)}">` +
`${STATUS_LABEL[player.status] || "—"}</span></span>`;
if (player.rank_total || player.rank_military) {
h += `<span class="chip"><span class="k">Classifica</span> <span class="v">` +
`${icon("trophy", 13)} #${player.rank_total ?? "—"} · ${icon("swords", 13)} #${player.rank_military ?? "—"}</span></span>`;
}
if (player.alliance_tag) {
h += `<span class="chip"><span class="k">Alleanza</span> ` +
`<span class="v">[${esc(player.alliance_tag)}]</span> ${esc(player.alliance_name || "")}</span>`;
}
h += `<span class="chip"><span class="k">Spie</span> <span class="v">${reports.length}</span></span>`;
h += `</div>`;
// pianeti del giocatore
h += `<div class="sec"><h4>Pianeti <span class="count-pill">${planets.length}</span></h4>`;
h += `<div class="kv">` + planets.map((o) =>
`<span class="chip chip-other" data-coords="${esc(o.coords)}">${artSVG(o.coords, 17)}` +
`${esc(o.name)} [${esc(o.coords)}]${o.moon ? " " + artSVG(o.coords, 13, true) : ""}</span>`
).join("") + `</div></div>`;
// tutti i rapporti del giocatore (pianeti e lune)
h += `<div class="sec"><h4>Rapporti di spionaggio <span class="count-pill">${reports.length}</span></h4>` +
(reports.length
? `<div class="rpt-grid">${reports.map(rptCard).join("")}</div>`
: `<div class="empty-note">Nessun rapporto salvato su questo giocatore.</div>`) +
`</div>`;
$("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 `<div class="rpt-card" data-sr="${esc(rec.sr_id)}">` +
`<div class="top"><span class="act-dot ${actClass(rec.activity)}"></span>` +
`<span class="rpt-date">${fmtEv(rec.event_time)}</span>` +
`<span class="rpt-kind ${rec.moon ? "moon" : "planet"}">${rec.moon ? "luna" : "pianeta"}</span></div>` +
`<div class="rpt-body"><div class="rpt-target">${esc(rec.defender_planet_name || "?")}` +
(rec.coords ? ` <span class="rpt-coords">[${esc(rec.coords)}]</span>` : "") +
`</div><div class="rpt-from">da ${esc(rec.attacker_name || "?")}` +
(loot ? ` · bottino ${rec.loot_percentage}%` : "") + `</div></div>` +
`<div class="rpt-foot"><span class="meta-dots">${parts.map((p) => `<span class="m">${p}</span>`).join("") || "—"}</span>` +
`<span class="rpt-open">Apri ›</span></div></div>`;
}
/* ---------------- 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 += `<div class="rpt-actions">` +
`<button class="back-btn" id="btn-back">${icon("ui_chev_l", 14)} Indietro al pianeta</button>` +
`<button class="del-btn" id="btn-del">${icon("ui_trash", 14)} Elimina rapporto</button>` +
`</div>`;
h += `<div class="kv">`;
h += `<span class="chip"><span class="k">Difensore</span> <span class="v">${esc(meta.defender_name || "?")}</span></span>`;
h += `<span class="chip"><span class="k">Attaccante</span> <span class="v">${esc(gen.attacker_name || "?")}</span></span>`;
h += `<span class="chip"><span class="k">Attività</span> <span class="v">${actLabel(meta.activity)}</span></span>`;
if (meta.loot_percentage !== null && meta.loot_percentage !== undefined)
h += `<span class="chip"><span class="k">Bottino</span> <span class="v">${meta.loot_percentage}%</span></span>`;
if (gen.spy_fail_chance !== undefined)
h += `<span class="chip"><span class="k">Rischio fall.</span> <span class="v">${gen.spy_fail_chance}%</span></span>`;
if (gen.debris_metal || gen.debris_crystal)
h += `<span class="chip"><span class="k">Campo detriti</span> <span class="v">${icon("extra_debris", 13)} ${num(gen.debris_metal)} / ${num(gen.debris_crystal)}</span></span>`;
h += `</div>`;
const rt = (lab, ico, val, cls) =>
`<div class="res-tile ${cls}"><span class="r-ico">${ico}</span><span>` +
`<div class="r-lab">${lab}</div><div class="r-val">${val}</div></span></div>`;
h += `<div class="res-grid">`;
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 += `</div>`;
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 += `<div class="warn">${icon("ui_warn", 14)} Spionaggio fallito su: ${failed.join(", ")} — dati incompleti.</div>`;
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 `<div class="tcard cat-${cat}" title="${esc(itemName(cat, tid))}">` +
`<span class="tico">${icon(itemIconName(cat, tid), 19)}</span>` +
`<span class="tname">${esc(itemName(cat, tid))}</span>` +
`<span class="tval">${val}</span></div>`;
}).join("");
return `<div class="sec"><h4>${title} <span class="count-pill">${rows.length}</span></h4>` +
`<div class="tech-grid">${cards}</div></div>`;
}
/* ---------------- 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) + ' <span>Nuovo rapporto</span>';
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();
})();
+107
View File
@@ -0,0 +1,107 @@
/* Utility condivise tra le pagine (galassia e classifiche) */
"use strict";
/* ---------------- utility base ---------------- */
function esc(s) {
return String(s ?? "").replace(/[&<>"']/g, (c) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
function num(v) { return (v === null || v === undefined) ? "—" : Number(v).toLocaleString("it"); }
function hashStr(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return Math.abs(h); }
async function api(path, opts) {
const r = await fetch(path, opts);
let data = null;
try { data = await r.json(); } catch (_) { /* noop */ }
if (!r.ok) throw new Error((data && data.error) || ("Errore HTTP " + r.status));
return data;
}
/* ---------------- icone SVG (window.ICONS da icons.js) ---------------- */
function icon(name, size = 16) {
const it = (window.ICONS || {})[name];
if (!it) return "";
return `<svg class="ic" width="${size}" height="${size}" viewBox="${it.vb}" ` +
`xmlns="http://www.w3.org/2000/svg" aria-hidden="true">${it.body}</svg>`;
}
/* ---------------- avatar pianeti/lune (SVG) ---------------- */
let _svgSeq = 0;
const PLANET_PAL = [
["#ffd9a0", "#e0762c"], ["#f2b3a0", "#b3422f"], ["#ffb1c0", "#a0275a"],
["#9fd8ff", "#2a6fc4"], ["#8fe8c0", "#1e7a5f"], ["#d3b8ff", "#6d3fc4"],
["#f5e3a0", "#c2a22e"], ["#c3d0e2", "#5c6f8c"],
];
const LUNA_PAL = [["#e6e6e6", "#9a9a9a"]];
function artSVG(seedKey, size, moon = false) {
const seed = hashStr(seedKey) || 1;
const r = 15; // viewBox 32
const pal = (moon ? LUNA_PAL : PLANET_PAL)[seed % (moon ? 1 : PLANET_PAL.length)];
const id = "g" + (++_svgSeq);
let extras = "";
if (moon) {
for (let i = 0; i < 4; i++) {
const cx = 7 + ((seed >> (i * 3)) % 18);
const cy = 7 + ((seed >> (i * 5 + 2)) % 18);
const cr = 2 + ((seed >> (i * 2 + 1)) % 3);
extras += `<circle cx="${cx}" cy="${cy}" r="${cr}" fill="rgba(0,0,0,.18)"/>` +
`<circle cx="${cx}" cy="${cy}" r="${cr}" fill="none" stroke="rgba(255,255,255,.28)" stroke-width=".7"/>`;
}
} else {
for (let i = 0; i < 3; i++) {
const cx = 8 + ((seed >> (i * 4)) % 14);
const cy = 6 + ((seed >> (i * 3 + 1)) % 14);
extras += `<circle cx="${cx}" cy="${cy}" r="${5 + ((seed >> i) % 5)}" fill="rgba(255,255,255,.10)"/>`;
}
}
return `<svg width="${size}" height="${size}" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">` +
`<defs><radialGradient id="${id}" cx=".35" cy=".3" r=".95">` +
`<stop offset="0" stop-color="${pal[0]}"/><stop offset="1" stop-color="${pal[1]}"/>` +
`</radialGradient></defs>` +
`<circle cx="16" cy="16" r="${r}" fill="url(#${id})"/>` +
extras +
`<path d="M2 17a14 14 0 0 0 28 0 12 9 0 0 1 -28 0z" fill="rgba(0,0,0,.30)"/>` +
`<circle cx="16" cy="16" r="${r}" fill="none" stroke="rgba(0,0,0,.35)" stroke-width="1"/>` +
`</svg>`;
}
/* ---------------- sfondo stellare ---------------- */
function initStars() {
const cv = document.getElementById("space");
if (!cv) return;
let ctx = null;
try { ctx = cv.getContext("2d"); } catch (_) { /* noop */ }
if (!ctx) return; // mai bloccare il resto dell'app
let stars = [], tw = [];
function resize() {
cv.width = innerWidth; cv.height = innerHeight;
stars = []; tw = [];
const n = Math.floor((cv.width * cv.height) / 6500);
for (let i = 0; i < n; i++) {
stars.push({ x: Math.random() * cv.width, y: Math.random() * cv.height,
r: Math.random() * 1.1 + .25, a: Math.random() * .5 + .2 });
}
for (let i = 0; i < 26; i++) {
tw.push({ x: Math.random() * cv.width, y: Math.random() * cv.height,
r: Math.random() * 1.2 + .4, p: Math.random() * Math.PI * 2, s: .8 + Math.random() });
}
}
resize();
addEventListener("resize", resize);
(function draw(t) {
ctx.clearRect(0, 0, cv.width, cv.height);
for (const s of stars) {
ctx.globalAlpha = s.a; ctx.fillStyle = "#cfe0ff";
ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, 7); ctx.fill();
}
const k = t / 1000;
for (const s of tw) {
ctx.globalAlpha = .25 + .6 * Math.abs(Math.sin(k * s.s + s.p));
ctx.fillStyle = "#fff";
ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, 7); ctx.fill();
}
ctx.globalAlpha = 1;
requestAnimationFrame(draw);
})(performance.now());
}
+2
View File
File diff suppressed because one or more lines are too long
+89
View File
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Galaxy Viewer — OGame</title>
<link rel="stylesheet" href="/static/style.css?v=5">
</head>
<body>
<canvas id="space" aria-hidden="true"></canvas>
<div id="app">
<header id="topbar">
<div class="brand">
<span class="logo"><svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="2"/><ellipse cx="12" cy="12" rx="10" ry="3.4" fill="none" stroke="currentColor" stroke-width="1.4" transform="rotate(-22 12 12)"/></svg></span>
<span>
<span id="universe-name">Galaxy Viewer</span>
<span id="universe-sub" class="dim"></span>
</span>
</div>
<div class="tools">
<div class="nav-group">
<button id="btn-prev-sys" title="Sistema precedente"><svg width="15" height="15" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 5.5L8 12l6.5 6.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<label>G <input id="inp-galaxy" type="number" min="1" value="1"></label>
<span class="dim">/ <span id="max-galaxy">?</span></span>
<label>S <input id="inp-system" type="number" min="1" value="1"></label>
<span class="dim">/ <span id="max-system">?</span></span>
<button id="btn-next-sys" title="Sistema successivo"><svg width="15" height="15" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M9.5 5.5L16 12l-6.5 6.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
</div>
<button id="btn-refresh" title="Ricarica la vista"><svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20 12a8 8 0 1 1-2.34-5.66" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M20 3.5V8h-4.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<a class="toplink" href="/rankings" title="Classifiche e ricerca giocatori"><span class="tl-ico"></span> Classifiche</a>
</div>
</header>
<section id="meta-bar">
<span id="meta-data-age" class="dim"></span>
<span id="meta-reports" class="dim"></span>
<span id="nav-status"></span>
</section>
<section id="import-box">
<span class="lbl" id="import-label">Nuovo rapporto</span>
<input id="inp-token" type="text" spellcheck="false"
placeholder="API string, es. sr-ar-170-98018ad7...">
<button id="btn-import">Importa</button>
<span id="import-msg"></span>
</section>
<main id="galaxy-wrap">
<table id="galaxy">
<thead>
<tr>
<th class="c-pos">Pos</th>
<th>Pianeta</th>
<th>Luna</th>
<th>Giocatore</th>
<th>Alleanza</th>
<th class="c-rep">Spie</th>
</tr>
</thead>
<tbody id="galaxy-body"></tbody>
</table>
<div id="empty-state" class="empty-note hidden">Nessun pianeta in questo sistema.</div>
</main>
<footer class="dim">API pubblica OGame · aggiornamento giornaliero · rapporti archiviati localmente · icone: <a href="https://game-icons.net" target="_blank" rel="noopener">game-icons.net</a> (CC BY 3.0)</footer>
</div>
<!-- Popup pianeta / rapporto -->
<div id="overlay" class="hidden">
<div id="popup">
<header id="popup-head">
<div id="popup-avatar" class="avatar"></div>
<div>
<div id="popup-title"></div>
<div id="popup-sub"></div>
</div>
<div class="spacer"></div>
<button class="icon-btn" id="popup-close" title="Chiudi"><svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg></button>
</header>
<div id="popup-body"></div>
</div>
</div>
<script src="/static/icons.js?v=1"></script>
<script src="/static/common.js?v=5"></script>
<script src="/static/app.js?v=5"></script>
</body>
</html>
+63
View File
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Classifiche — Galaxy Viewer</title>
<link rel="stylesheet" href="/static/style.css?v=5">
</head>
<body>
<canvas id="space" aria-hidden="true"></canvas>
<div id="app">
<header id="topbar">
<div class="brand">
<span class="logo" id="rk-logo"></span>
<span>
<span id="rk-universe">Classifiche</span>
<span id="rk-sub" class="dim"></span>
</span>
</div>
<div class="tools">
<a class="toplink" href="/">◀ Galassia</a>
</div>
</header>
<div id="rk-head">
<div class="rk-tabs" id="rk-tabs">
<button class="rk-tab on" data-type="total">Generale</button>
<button class="rk-tab" data-type="military">Armamenti</button>
<button class="rk-tab" data-type="fleet">Top Flotte</button>
</div>
</div>
<section id="rk-filter">
<span class="lbl">Stato</span>
<select id="f-status">
<option value="all">Tutti</option>
<option value="active">Attivi</option>
<option value="inactive">Inattivi</option>
</select>
<span class="lbl rg">Posizione</span>
<div class="rg">
<input id="f-from" type="number" min="1" placeholder="da">
<span class="dim">–</span>
<input id="f-to" type="number" min="1" placeholder="a">
</div>
<input id="f-q" type="search" placeholder="Cerca nome o alleanza…">
<button id="f-apply">Filtra</button>
<button id="f-reset">Azzera</button>
<span id="f-msg"></span>
</section>
<div id="rk-count"></div>
<div id="rk-results"></div>
<footer class="dim">Classifiche aggiornate ogni ora · API pubblica OGame · icone: <a href="https://game-icons.net" target="_blank" rel="noopener">game-icons.net</a> (CC BY 3.0)</footer>
</div>
<script src="/static/icons.js?v=1"></script>
<script src="/static/common.js?v=5"></script>
<script src="/static/rankings.js?v=5"></script>
</body>
</html>
+130
View File
@@ -0,0 +1,130 @@
/* Classifiche — logica pagina */
"use strict";
const $ = (id) => document.getElementById(id);
const S = { type: "total", status: "all", q: "", from: 0, to: 0 };
const STATUS_LABEL = { "": "Attivo", i: "Inattivo", I: "Inattivo lungo", v: "In vacanza",
vi: "Vacanza + inatt.", vI: "Vacanza + inatt.", a: "Admin" };
const stClass = (s) => "st-" + (s || "");
async function initHead() {
try {
const b = await api("/api/bootstrap");
$("rk-universe").textContent = `${b.server.name} — Classifiche`;
$("rk-sub").textContent =
`[s${b.server.number}-${b.server.community}] · aggiornamento: ${b.fetched.highscore_total ? b.fetched.highscore_total : "—"} UTC`;
} catch (e) { /* header non bloccante */ }
}
function fmtScore(v) {
if (v === null || v === undefined) return "—";
return Number(v).toLocaleString("it-IT");
}
function posClass(rank) {
if (!rank) return "pos na";
return "pos p" + rank;
}
async function load() {
const msg = $("f-msg");
msg.textContent = "Caricamento…";
const p = new URLSearchParams({ type: S.type, status: S.status, q: S.q });
if (S.from > 0) p.set("start", S.from);
if (S.to > 0) p.set("end", S.to);
try {
const d = await api("/api/rankings?" + p.toString());
render(d);
msg.textContent = "";
} catch (e) {
msg.textContent = e.message;
}
}
function render(d) {
const isFleet = d.type === "fleet";
const kindLabel = {
total: "classifica generale",
military: "classifica armamenti",
fleet: "top flotte — navi dai rapporti",
}[d.type] || d.type;
$("rk-count").innerHTML = `Giocatori: <b>${d.count}</b> — ` +
`<span class="dim">${kindLabel}</span>`;
const rows = d.rows.map((r) => {
const st = r.status || "";
const stLab = STATUS_LABEL[st] || st;
const jump = r.avatar ? `onclick="goGalaxy('${r.avatar}')"` : "";
const mine = r.mine ? ' <span title="sei tu" class="mymark">●</span>' : "";
const empty = r.avatar
? `<span class="rk-avatar" ${jump} title="Vai al pianeta ${esc(r.avatar)}">${artSVG(r.avatar, 26)}</span>`
: `<span class="rk-avatar">${artSVG(r.name + r.id, 26)}</span>`;
const rep = r.reports || 0;
const repCoords = r.report_coords || r.avatar;
const repCell = rep
? `<span class="rep-badge"${repCoords ? ` onclick="goGalaxy('${esc(repCoords)}')"` : ""} ` +
`title="${rep} rapport${rep === 1 ? "o" : "i"} di spionaggio su questo giocatore">` +
`${icon("badge_spy", 12)} ${rep}</span>`
: `<span class="dim">—</span>`;
return `<tr class="${r.mine ? "mine" : ""}">` +
`<td class="${posClass(r.rank)}">${r.rank ? "#" + r.rank : "—"}</td>` +
`<td>${empty}</td>` +
`<td><span class="rk-name ${stClass(st)}" ${jump} title="Vai al pianeta">${esc(r.name)}</span>${mine}</td>` +
`<td class="rk-ally">${r.alliance_tag ? "[" + esc(r.alliance_tag) + "]" : ""}</td>` +
`<td><span class="pname-status ${stClass(st)}">${esc(stLab)}</span></td>` +
`<td class="planets-n">${r.planets || 0}</td>` +
`<td style="text-align:center">${repCell}</td>` +
`<td class="score">${fmtScore(r.score)}<small>${r.score ? (isFleet ? "navi" : "punti") : ""}</small></td>` +
`</tr>`;
}).join("");
const head =
`<thead><tr>` +
`<th style="width:56px">Pos</th><th style="width:40px"></th>` +
`<th>Giocatore</th><th>Alleanza</th><th>Stato</th>` +
`<th style="text-align:center">Pianeti</th><th style="text-align:center">Spie</th>` +
`<th style="text-align:right">${isFleet ? "Navi" : "Punti"}</th>` +
`</tr></thead>`;
$("rk-results").innerHTML =
rows
? `<table class="rank-table">${head}<tbody>${rows}</tbody></table>`
: `<div class="empty-note">Nessun giocatore corrisponde ai filtri.</div>`;
}
function goGalaxy(coords) {
const [g, s] = coords.split(":").map(Number);
location.href = `/?g=${g}&s=${s}`;
}
/* ---------------- eventi ---------------- */
const TABS = { total: ["trophy", "Generale"], military: ["swords", "Armamenti"], fleet: ["cat_ships", "Top Flotte"] };
$("rk-tabs").querySelectorAll(".rk-tab").forEach((t) => {
const [ic, label] = TABS[t.dataset.type] || ["trophy", t.dataset.type];
t.innerHTML = icon(ic, 15) + " " + label;
t.onclick = () => {
$("rk-tabs").querySelectorAll(".rk-tab").forEach((x) => x.classList.remove("on"));
t.classList.add("on");
S.type = t.dataset.type;
load();
};
});
function readFilters() {
S.status = $("f-status").value;
S.from = parseInt($("f-from").value, 10) || 0;
S.to = parseInt($("f-to").value, 10) || 0;
S.q = $("f-q").value.trim();
}
$("f-apply").onclick = () => { readFilters(); load(); };
$("f-reset").onclick = () => {
$("f-status").value = "all"; $("f-from").value = ""; $("f-to").value = ""; $("f-q").value = "";
readFilters(); load();
};
["f-q", "f-from", "f-to"].forEach((id) => {
$(id).addEventListener("keydown", (e) => { if (e.key === "Enter") { readFilters(); load(); } });
});
/* ---------------- init ---------------- */
initStars();
const lg = document.getElementById("rk-logo");
if (lg) lg.innerHTML = icon("trophy", 20);
initHead();
load();
+498
View File
@@ -0,0 +1,498 @@
/* OGame Galaxy Viewer v2 — stile spaziale, ispirato a OGame/galaxytool */
:root {
--bg0: #04060c;
--bg1: #0a0f1a;
--bg2: #101827;
--bg3: #17223a;
--panel: rgba(16, 24, 39, .86);
--line: #26344e;
--line-soft: #1b2740;
--text: #d6e0f2;
--text-dim: #7483a0;
--accent: #57a6ff;
--green: #52d273;
--amber: #f0c94e;
--orange: #ff9a3d;
--red: #ff5d5d;
--blue: #7ab8ff;
--radius: 10px;
--shadow: 0 8px 30px rgba(0,0,0,.55);
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
background: var(--bg0);
color: var(--text);
font-family: "Segoe UI", "Helvetica Neue", Verdana, Arial, sans-serif;
font-size: 13.5px;
min-height: 100%;
}
/* ---------- sfondo spaziale ---------- */
#space {
position: fixed; inset: 0; width: 100%; height: 100%;
z-index: 0; pointer-events: none;
}
body::before {
content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none;
background:
radial-gradient(900px 420px at 18% -8%, rgba(40,90,170,.20), transparent 60%),
radial-gradient(800px 400px at 85% 110%, rgba(120,60,190,.13), transparent 60%),
radial-gradient(1000px 600px at 60% 30%, rgba(20,40,80,.18), transparent 55%);
}
#app {
position: relative; z-index: 1;
max-width: 1120px; margin: 0 auto; padding: 18px 16px 40px;
}
.dim { color: var(--text-dim); }
.hidden { display: none !important; }
/* ---------- header ---------- */
#topbar {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; flex-wrap: wrap;
background: linear-gradient(180deg, rgba(30,44,74,.95), rgba(13,20,36,.92));
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 12px 16px;
box-shadow: var(--shadow);
backdrop-filter: blur(6px);
}
.brand { display: flex; align-items: center; gap: 11px; font-weight: 700; font-size: 16px; letter-spacing: .3px; }
.logo {
width: 34px; height: 34px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
background: radial-gradient(circle at 32% 30%, #6fb1ff, #1a3a6b 70%);
color: #fff; font-size: 17px;
box-shadow: 0 0 0 1px rgba(120,180,255,.5), 0 0 18px rgba(60,140,255,.35);
}
#universe-sub { font-size: 11px; font-weight: 400; opacity: .8; display:block; margin-top:2px;}
.tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.nav-group {
display: flex; align-items: center; gap: 4px;
background: rgba(6,10,18,.6); border: 1px solid var(--line);
border-radius: 8px; padding: 4px 8px;
}
.nav-group label { display: flex; align-items: center; gap: 3px; font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .4px;}
.nav-group input {
width: 54px; background: #0b1220; color: var(--text);
border: 1px solid #2b3b5c; border-radius: 6px;
padding: 5px 4px; text-align: center; font-size: 13px; font-weight: 600;
}
.nav-group input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(87,166,255,.25); }
button {
background: linear-gradient(180deg, #2c3f63, #1b2a47);
color: var(--text); border: 1px solid #33466e;
border-radius: 8px; padding: 6px 13px; cursor: pointer; font-size: 13px;
transition: filter .15s, transform .05s;
}
button:hover { filter: brightness(1.25); }
button:active { transform: translateY(1px); }
button:disabled { opacity: .5; cursor: default; }
#btn-refresh { padding: 6px 10px; font-size: 15px; line-height: 1; }
.toplink {
background: linear-gradient(180deg,#2c3f63,#1b2a47);
border: 1px solid #33466e; border-radius: 8px;
color: var(--text); padding: 6px 13px; text-decoration: none; font-size: 13px;
}
.toplink:hover { filter: brightness(1.25); color: #fff; }
.toplink.back { color: var(--text-dim); }
/* ---------- meta / import ---------- */
#meta-bar {
display: flex; justify-content: space-between; gap: 10px;
padding: 10px 6px 4px; font-size: 11px; flex-wrap: wrap;
}
#nav-status { color: var(--accent); font-size: 12px; font-weight: 600; }
#meta-reports b { color: var(--amber); }
#import-box {
display: flex; gap: 8px; align-items: center;
background: var(--panel);
border: 1px dashed #33466e;
border-radius: var(--radius);
padding: 9px 12px; margin: 6px 0 16px;
flex-wrap: wrap;
box-shadow: 0 2px 12px rgba(0,0,0,.3);
}
#import-box .lbl { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing:.5px; }
#import-box input {
flex: 1 1 340px; background: #070c16; color: var(--text);
border: 1px solid #2b3b5c; border-radius: 7px; padding: 7px 11px;
font-family: "Cascadia Mono", "Courier New", monospace; font-size: 12px;
}
#import-box input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(87,166,255,.22); }
#import-msg { font-size: 12px; }
#import-msg.ok { color: var(--green); }
#import-msg.err { color: var(--red); }
#btn-import { background: linear-gradient(180deg,#2e7dd1,#1d5aa6); border-color:#3f92e8; font-weight:600; }
/* niente freccette sugli input numerici (solo digitazione) */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
/* ---------- galassia ---------- */
#galaxy-wrap { overflow-x: auto; }
#galaxy {
width: 100%; border-collapse: separate; border-spacing: 0;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
backdrop-filter: blur(4px);
}
#galaxy thead th {
background: linear-gradient(180deg, rgba(36,52,86,.9), rgba(22,32,56,.9));
color: #a9bcde; font-weight: 700; font-size: 10.5px;
text-transform: uppercase; letter-spacing: 1px;
padding: 9px 10px; text-align: left;
border-bottom: 1px solid #2c3f63;
}
#galaxy tbody td {
padding: 5px 10px; vertical-align: middle;
border-bottom: 1px solid rgba(38,52,78,.45);
font-size: 12.5px;
}
#galaxy tbody tr:last-child td { border-bottom: none; }
#galaxy tbody tr:nth-child(even) td { background: rgba(255,255,255,.02); }
#galaxy tbody tr:hover td { background: rgba(46,78,133,.22); }
td.c-pos { width: 40px; color: var(--text-dim); font-weight: 700; text-align:center; }
td.c-rep { width: 84px; }
.pcell { display: flex; align-items: center; gap: 9px; cursor: pointer; padding: 2px 4px; border-radius: 8px; }
.pcell:hover { background: rgba(70,110,180,.25); box-shadow: inset 0 0 0 1px rgba(87,166,255,.35); }
.pcell .p-avatar { flex: 0 0 auto; filter: drop-shadow(0 2px 4px rgba(0,0,0,.6)); }
.pname { font-weight: 700; color: #cfe1ff; letter-spacing:.2px; }
.ro-tag {
display:inline-block; margin-left:5px; padding:0 5px; vertical-align:1px;
font-size:9px; text-transform:uppercase; letter-spacing:.4px;
color:#c9a24a; border:1px solid rgba(201,162,74,.5); border-radius:8px;
}
.pcoords { color: var(--text-dim); font-size: 10px; }
.planet-mine .pname { color: var(--green); }
.planet-mine .pname::after { content: " ●"; color: var(--green); font-size: 9px; vertical-align: top; }
.mymark { color: var(--green); font-size: 9px; vertical-align: top; }
.mcell { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; padding: 2px 6px; border-radius: 6px; color: var(--text-dim); }
.mcell:hover { background: rgba(70,110,180,.2); }
.pname-status { font-weight: 600; }
.pname-status.player-link { cursor: pointer; }
.pname-status.player-link:hover { text-decoration: underline; filter: brightness(1.18); }
.pranks {
margin-left: 8px; font-size: 10.5px; color: var(--text-dim);
white-space: nowrap; vertical-align: middle;
}
.pranks .rk { display: inline-block; margin-right: 7px; letter-spacing:.3px; }
/* ---------- pagina classifiche ---------- */
#rk-head { margin: 0 0 4px; }
.rk-tabs { display: inline-flex; background: rgba(10,15,26,.7); border: 1px solid var(--line); border-radius: 10px; padding: 3px; gap: 3px; }
.rk-tab {
border: none; background: none; color: var(--text-dim); font-weight: 700;
border-radius: 8px; padding: 8px 18px; font-size: 13px; cursor: pointer;
}
.rk-tab.on { background: linear-gradient(180deg,#2c3f63,#1b2a47); color: #fff; }
.rk-tab:hover:not(.on) { color: #fff; }
#rk-filter {
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
background: var(--panel); border: 1px solid var(--line);
border-radius: var(--radius); padding: 10px 12px; margin: 12px 0 6px;
box-shadow: 0 2px 12px rgba(0,0,0,.3);
}
#rk-filter .lbl { font-size: 10.5px; color: var(--text-dim); text-transform: uppercase; letter-spacing:.5px; }
#rk-filter input, #rk-filter select {
background: #070c16; color: var(--text); border: 1px solid #2b3b5c;
border-radius: 7px; padding: 6px 8px; font-size: 12.5px;
}
#rk-filter input:focus, #rk-filter select:focus { outline:none; border-color: var(--accent); }
#rk-filter .rg { display:flex; align-items:center; gap:5px; }
#rk-filter input[type=number] { width: 70px; }
#rk-filter input[type=search] { width: 190px; }
#rk-count { font-size: 11.5px; color: var(--text-dim); margin: 8px 4px 4px; }
#rk-count b { color: var(--amber); }
#rk-results { overflow-x: auto; margin-top: 4px; }
table.rank-table {
width: 100%; border-collapse: separate; border-spacing: 0;
background: var(--panel); border: 1px solid var(--line);
border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow);
}
table.rank-table thead th {
background: linear-gradient(180deg, rgba(36,52,86,.9), rgba(22,32,56,.9));
color: #a9bcde; font-size: 10.5px; text-transform: uppercase; letter-spacing: 1px;
padding: 9px 12px; text-align: left; border-bottom: 1px solid #2c3f63;
}
table.rank-table td {
padding: 6px 12px; border-bottom: 1px solid rgba(38,52,78,.4); font-size: 12.5px;
vertical-align: middle;
}
table.rank-table tr:nth-child(even) td { background: rgba(255,255,255,.02); }
table.rank-table tbody tr:hover td { background: rgba(46,78,133,.22); }
table.rank-table tr:last-child td { border-bottom: none; }
table.rank-table tr.mine td { background: rgba(87,166,255,.10); }
.pos { font-weight: 800; text-align:center; width: 56px; }
.pos.p1 { color: #f7d154; text-shadow: 0 0 8px rgba(247,209,84,.4); }
.pos.p2 { color: #cfd6e4; }
.pos.p3 { color: #e09b62; }
.pos.na { color: #56617a; }
.rk-avatar { cursor: pointer; filter: drop-shadow(0 1px 3px rgba(0,0,0,.6)); }
.rk-avatar:hover { filter: brightness(1.2) drop-shadow(0 0 6px rgba(87,166,255,.5)); }
.rk-name { font-weight: 700; cursor: pointer; }
.rk-name:hover { color: var(--accent); text-decoration: underline; }
.rk-ally { color: #8fa3c7; font-size: 11.5px; }
.score { font-weight: 700; color: #eef3ff; text-align:right; }
.score small { display:block; color: var(--text-dim); font-weight:400; font-size:10px; }
.planets-n { color: var(--text-dim); text-align:center; }
/* ---------- colori stato giocatore (galaxy OGame) ---------- */
.st- { color: #f7d154; } /* attivo */
.st-i, .st-I, .st-vi, .st-vI { color: #9aa4b5; } /* inattivo / vacanza+inattivo */
.st-v { color: #6ec6ff; } /* in vacanza (celeste) */
.st-a { color: #ff5555; } /* admin / staff */
.ally-tag { color: #8fa3c7; font-size: 11.5px; }
.rep-badge {
display: inline-flex; align-items: center; gap: 5px;
background: rgba(210,45,45,.9); color: #fff;
border-radius: 20px; padding: 2px 10px; font-size: 11px; font-weight: 700;
cursor: pointer; border: 1px solid #e26a66;
box-shadow: 0 1px 6px rgba(210,45,45,.5);
transition: filter .15s;
}
.rep-badge:hover { filter: brightness(1.2); }
.rep-badge.rep-moon { background: rgba(98,74,158,.95); border-color:#a58bd6; box-shadow: 0 1px 6px rgba(98,74,158,.5); }
.rep-badge .ico { font-size: 12px; }
/* ---------- popup ---------- */
#overlay {
position: fixed; inset: 0; z-index: 50;
background: rgba(2,4,10,.82);
display: flex; align-items: flex-start; justify-content: center;
padding: 30px 12px; overflow-y: auto;
backdrop-filter: blur(3px);
animation: fadeIn .15s ease;
}
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
#popup {
width: 920px; max-width: 100%;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: 0 24px 80px rgba(0,0,0,.75);
animation: popIn .18s ease;
overflow: hidden;
}
@keyframes popIn { from { transform: translateY(14px); opacity: 0 } to { transform: none; opacity: 1 } }
#popup-head {
display: flex; align-items: center; gap: 12px;
background: linear-gradient(180deg, rgba(32,46,80,.96), rgba(16,24,42,.95));
border-bottom: 1px solid var(--line);
padding: 12px 18px;
}
#popup-head .avatar { flex: 0 0 auto; filter: drop-shadow(0 3px 8px rgba(0,0,0,.6)); }
#popup-title { font-weight: 800; font-size: 16px; letter-spacing:.3px; line-height: 1.25; }
#popup-sub { font-size: 11.5px; color: var(--text-dim); margin-top: 2px; }
.spacer { flex: 1 1 auto; }
.icon-btn {
background: rgba(255,255,255,.05); border: 1px solid var(--line);
border-radius: 8px; width: 32px; height: 32px; flex: 0 0 auto; padding: 0;
display: inline-flex; align-items: center; justify-content: center;
font-size: 15px; color: var(--text-dim); cursor: pointer;
}
.icon-btn:hover { color: #fff; background: rgba(255,255,255,.1); }
#popup-body { padding: 6px 18px 22px; max-height: 76vh; overflow-y: auto; }
#popup-body::-webkit-scrollbar { width: 9px; }
#popup-body::-webkit-scrollbar-thumb { background: #2b3b5c; border-radius: 8px; }
/* chips informazioni giocatore */
.kv { display: flex; flex-wrap: wrap; gap: 8px 10px; margin: 10px 0 4px; }
.chip {
display: inline-flex; align-items: center; gap: 7px;
background: rgba(255,255,255,.045);
border: 1px solid var(--line-soft); border-radius: 20px;
padding: 5px 12px; font-size: 12px;
}
.chip .k { color: var(--text-dim); text-transform: uppercase; letter-spacing:.5px; font-size: 9.5px; }
.chip .v { font-weight: 700; }
/* altri pianeti */
.chip-other { cursor: pointer; transition: background .12s, border-color .12s; }
.chip-other:hover { background: rgba(70,110,180,.25); border-color: var(--accent); }
.chip-other svg { vertical-align: middle; }
/* ---------- sezioni ---------- */
.sec { margin: 14px 0 4px; }
.sec h4 {
margin: 16px 0 8px; font-size: 11px; color: #8fa6cc; font-weight: 800;
letter-spacing: 1.1px; text-transform: uppercase;
display: flex; align-items: center; gap: 8px;
}
.sec h4::after { content:""; flex:1; height:1px; background: linear-gradient(90deg, var(--line), transparent); }
.count-pill {
background: rgba(87,166,255,.15); color: var(--accent);
border-radius: 20px; padding: 0 8px; font-size: 10px; font-weight:700;
}
/* icone svg (icons.js) */
.ic { display: inline-block; flex: none; vertical-align: -2px; }
button svg, .icon-btn svg, .toplink svg, .logo svg, .lbl svg,
.chip svg, .rk svg, .pranks svg, .rep-badge svg, .rpt-foot svg,
.meta-dots svg, .warn svg { vertical-align: -2px; }
/* ---------- card rapporto ---------- */
.rpt-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(255px, 1fr));
gap: 10px;
}
.rpt-card {
background: linear-gradient(180deg, rgba(24,33,55,.85), rgba(13,19,33,.9));
border: 1px solid var(--line-soft);
border-radius: 12px;
padding: 12px 13px;
cursor: pointer;
position: relative;
transition: transform .12s, border-color .15s, box-shadow .15s;
}
.rpt-card:hover {
transform: translateY(-2px);
border-color: rgba(87,166,255,.55);
box-shadow: 0 8px 22px rgba(0,0,0,.45);
}
.rpt-card .top { display: flex; align-items: center; gap: 8px; }
.rpt-date { font-weight: 700; font-size: 12.5px; }
.rpt-kind {
font-size: 9.5px; text-transform: uppercase; letter-spacing:.6px;
border-radius: 5px; padding: 1px 6px; font-weight:700;
}
.rpt-kind.planet { background: rgba(87,166,255,.18); color: var(--blue); }
.rpt-kind.moon { background: rgba(165,139,214,.18); color:#c9b5f0; }
.rpt-body { margin: 8px 0 2px; }
.rpt-target { font-weight: 700; color: #e8f0ff; }
.rpt-coords { color: var(--text-dim); font-weight: 500; font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.rpt-from { color: var(--text-dim); font-size: 11.5px; }
.rpt-foot {
display: flex; justify-content: space-between; align-items: center;
margin-top: 9px; padding-top: 8px; border-top: 1px dashed var(--line-soft);
color: var(--text-dim); font-size: 11px; flex-wrap: wrap; gap:4px;
}
.rpt-open { color: var(--accent); font-weight: 700; font-size: 11px; }
.meta-dots { display:flex; gap:4px; align-items:center; }
.meta-dots .m { min-width:15px; text-align:center; }
.act-dot { display:inline-block; width:9px; height:9px; border-radius:50%; box-shadow:0 0 6px currentColor; }
.act-0 { background:#ff3b30; color:#ff3b30; }
.act-15 { background:#ff9500; color:#ff9500; }
.act-30 { background:#ffd60a; color:#ffd60a; }
.act-60 { background:#30d158; color:#30d158; }
.act-none { background:#8e8e93; color:#8e8e93; }
/* dettaglio rapporto */
.detail-head { display:flex; align-items:center; gap:14px; margin: 12px 0 4px; }
.back-btn { display:inline-flex; align-items:center; gap:6px; background:none; border:1px solid var(--line); color:var(--text); border-radius:8px; padding:5px 11px; font-size:12px; cursor:pointer;}
.back-btn:hover { border-color: var(--accent); color:#fff; }
/* azioni nel dettaglio rapporto */
.rpt-actions { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin:4px 0 2px; }
.del-btn {
display:inline-flex; align-items:center; gap:6px;
background: rgba(210,45,45,.12); border:1px solid rgba(226,106,102,.55);
color:#ff8a80; border-radius:8px; padding:5px 11px; font-size:12px;
font-weight:700; cursor:pointer; transition: background .12s, color .12s;
}
.del-btn:hover { background: rgba(210,45,45,.25); color:#ffb0a8; border-color:#e26a66; }
.res-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 8px; margin: 10px 0;
}
.res-tile {
border-radius: 10px; padding: 9px 12px;
display: flex; align-items: center; gap: 10px;
border: 1px solid var(--line-soft);
background: rgba(255,255,255,.03);
}
.res-tile .r-ico { color: var(--rc, #9fb0cc); }
.res-tile .r-ico svg { width: 21px; height: 21px; }
.res-tile .r-lab { font-size: 9.5px; text-transform: uppercase; letter-spacing:.6px; color: var(--text-dim); }
.res-tile .r-val { font-weight: 800; font-size: 15px; }
.res-metal { --rc: #e3c5ff; }
.res-metal .r-val { color:#e3c5ff; }
.res-crystal { --rc: #7fd4ff; }
.res-crystal .r-val { color:#7fd4ff; }
.res-deuterium { --rc: #7bf0b8; }
.res-deuterium .r-val { color:#7bf0b8; }
.res-pop { --rc: #ffd9a0; }
.res-pop .r-val { color:#ffd9a0; }
.res-food { --rc: #c4f27b; }
.res-food .r-val { color:#c4f27b; }
/* griglia elementi tecnici */
.tech-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 7px;
}
.tcard {
display: flex; align-items: center; gap: 9px;
background: rgba(255,255,255,.035);
border: 1px solid var(--line-soft);
border-left: 3px solid var(--tc, #57a6ff);
border-radius: 9px;
padding: 6px 9px;
transition: background .12s, transform .12s;
}
.tcard:hover { background: rgba(255,255,255,.07); transform: translateY(-1px); }
.tcard .tico {
width: 30px; height: 30px; border-radius: 8px; flex: 0 0 auto;
display: flex; align-items: center; justify-content: center;
background: color-mix(in srgb, var(--tc,#57a6ff) 22%, transparent);
color: var(--tc, #57a6ff);
}
.tcard .tico svg { width: 19px; height: 19px; }
.tcard .tname { font-size: 11.5px; line-height: 1.2; flex: 1 1 auto; color:#dbe6f7; }
.tcard .tval { font-weight: 800; font-size: 14px; color: #fff; }
.tcard .tval small { font-weight:400; color:var(--text-dim); font-size:9.5px; }
.cat-buildings { --tc: #fbbf24; }
.cat-research { --tc: #38bdf8; }
.cat-ships { --tc: #c084fc; }
.cat-defense { --tc: #34d399; }
.cat-lfbuildings { --tc: #a3e635; }
.warn {
color: var(--amber); font-size: 11.5px; margin: 6px 0;
background: rgba(240,201,78,.08); border:1px solid rgba(240,201,78,.25);
border-radius: 8px; padding: 6px 10px;
}
.empty-note {
color: var(--text-dim); font-style: italic; padding: 10px 4px;
border: 1px dashed var(--line-soft); border-radius: 10px; text-align:center;
}
footer { text-align: center; margin-top: 22px; font-size: 11px; opacity:.7; }
@media (max-width: 720px) {
.kv { gap: 6px; }
.rpt-grid { grid-template-columns: 1fr; }
.tech-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
}