0.3.0: Freunde und Lobby, Server-Browser entfernt
- Seite Freunde: hinzufuegen, annehmen, online-Anzeige, Lobby erstellen/einladen/beitreten, Server starten - SPIELEN in der Lobby: Ticket fuer die Lobby + Relay-Adresse in BSO.json - Knopf 'Mod herunterladen' holt jederzeit die neueste Mod Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -140,7 +140,7 @@ function showPage(name) {
|
||||
document.querySelectorAll("#nav [data-page]").forEach((b) => b.classList.toggle("on", b.dataset.page === name));
|
||||
const root = $("page");
|
||||
root.replaceChildren(h("p", { class: "muted" }, "Laden …"));
|
||||
({ home: renderHome, servers: renderServers, news: renderNews, settings: renderSettings })[name](root);
|
||||
({ home: renderHome, friends: renderFriends, news: renderNews, settings: renderSettings })[name](root);
|
||||
}
|
||||
|
||||
// Anmeldung abgelaufen (z.B. beim Holen des Spiel-Tickets)
|
||||
@@ -210,8 +210,10 @@ function modCard(s) {
|
||||
const value = m.installed ? `Version ${m.installed}` : "Nicht installiert";
|
||||
const info = m.updateAvailable ? `Neu: ${m.latest}` : m.latest ? "Aktuell" : "Keine Version auf dem Server";
|
||||
const extra = [h("span", { class: "muted small" }, info)];
|
||||
if (m.updateAvailable && s.game.found) {
|
||||
const b = h("button", { class: "btn small", type: "button" }, m.installed ? "Aktualisieren" : "Installieren");
|
||||
// Immer da: holt die neueste Mod von der Website (bei SPIELEN passiert das sonst automatisch, wenn eingeschaltet)
|
||||
if (m.latest && s.game.found) {
|
||||
const label = !m.installed ? "Mod herunterladen" : m.updateAvailable ? "Mod aktualisieren" : "Neu herunterladen";
|
||||
const b = h("button", { class: "btn small" + (!m.installed || m.updateAvailable ? " primary" : ""), type: "button" }, label);
|
||||
b.addEventListener("click", () => busy(b, async () => {
|
||||
const r = await call("installMod");
|
||||
toast("BSO " + r.version + " installiert");
|
||||
@@ -243,64 +245,138 @@ function serverCard(s) {
|
||||
h("label", { class: "switch" }, h("span", {}, "Beim Start verbinden", h("small", {}, "sonst im Spiel mit F8")), auto));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Server-Browser
|
||||
// ---------------------------------------------------------------- Freunde & Lobby
|
||||
// Die Lobby liegt auf der Website, ihr Server laeuft im BSO-Relay - keiner hostet, keiner braucht eine Portfreigabe.
|
||||
|
||||
let serverList = [];
|
||||
let social = { friends: { friends: [], incoming: [], outgoing: [] }, lobby: { lobby: null, invites: [] } };
|
||||
const seenNotices = new Set();
|
||||
|
||||
async function renderServers(root) {
|
||||
const search = h("input", { placeholder: "Server suchen …", spellcheck: "false" });
|
||||
const refresh = h("button", { class: "btn small", type: "button" }, "Aktualisieren");
|
||||
const list = h("div", { class: "server-list" }, h("p", { class: "muted" }, "Server werden gesucht …"));
|
||||
const direct = h("input", { placeholder: "Adresse, z.B. bso.example.de:7777", spellcheck: "false" });
|
||||
const directJoin = h("button", { class: "btn small", type: "button" }, "Verbinden");
|
||||
directJoin.addEventListener("click", () => join(directJoin, direct.value));
|
||||
|
||||
root.replaceChildren(
|
||||
h("div", { class: "row", style: "justify-content:space-between;flex-wrap:wrap" }, h("h1", {}, "Server"),
|
||||
h("div", { class: "row", style: "min-width:320px" }, search, refresh)),
|
||||
list,
|
||||
h("div", { class: "card" }, h("h3", {}, "Direkt verbinden"), h("div", { class: "row" }, direct, directJoin)));
|
||||
|
||||
const draw = () => {
|
||||
const q = search.value.trim().toLowerCase();
|
||||
const rows = serverList.filter((s) => !q || s.name.toLowerCase().includes(q) || s.address.toLowerCase().includes(q));
|
||||
if (!rows.length) {
|
||||
list.replaceChildren(h("p", { class: "muted" }, serverList.length ? "Kein Server passt zur Suche." : "Gerade ist kein öffentlicher Server online."));
|
||||
return;
|
||||
}
|
||||
list.replaceChildren(...rows.map((s) => {
|
||||
const b = h("button", { class: "btn primary small", type: "button" }, "BEITRETEN");
|
||||
b.disabled = !s.compatible || s.players >= s.maxPlayers;
|
||||
b.addEventListener("click", () => join(b, s.address));
|
||||
const ping = s.ping == null ? h("span", { class: "muted" }, "–") : h("span", { class: s.ping < 80 ? "ok" : s.ping < 150 ? "warn" : "bad" }, s.ping + " ms");
|
||||
return h("div", { class: "server" },
|
||||
h("div", { class: "server-main" }, h("b", {}, s.name),
|
||||
h("span", { class: "muted small" }, s.description || s.address)),
|
||||
h("span", { class: "server-col" }, `${s.players}/${s.maxPlayers}`),
|
||||
h("span", { class: "server-col" }, ping),
|
||||
h("span", { class: "server-col muted small" }, s.compatible ? "v" + s.version : "andere Version"),
|
||||
b);
|
||||
}));
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
refresh.disabled = true;
|
||||
try { serverList = await call("servers"); draw(); }
|
||||
catch (e) { list.replaceChildren(h("p", { class: "error" }, e.message)); }
|
||||
finally { refresh.disabled = false; }
|
||||
};
|
||||
search.addEventListener("input", draw);
|
||||
refresh.addEventListener("click", load);
|
||||
await load();
|
||||
async function loadSocial() {
|
||||
const [friends, lobby] = await Promise.all([call("friends"), call("lobby")]);
|
||||
social = { friends, lobby };
|
||||
const n = friends.incoming.length + lobby.invites.length;
|
||||
$("friendsBadge").textContent = n;
|
||||
$("friendsBadge").classList.toggle("hidden", n === 0);
|
||||
// Neue Einladungen/Anfragen melden (auch auf anderen Seiten)
|
||||
for (const i of lobby.invites) notice("l" + i.lobbyId, `${i.owner} lädt dich in die Lobby ein`);
|
||||
for (const r of friends.incoming) notice("f" + r.id, `${r.username} möchte dein Freund sein`);
|
||||
}
|
||||
|
||||
/** Beitreten: Server merken, Spiel starten, im Spiel automatisch verbinden. */
|
||||
function join(button, address) {
|
||||
if (!address.trim()) { toast("Adresse eingeben", true); return; }
|
||||
busy(button, async () => {
|
||||
await call("join", { address: address.trim() });
|
||||
toast("Bus-Simulator 2012 startet und verbindet mit " + address.trim() + " …");
|
||||
function notice(key, text) {
|
||||
if (seenNotices.has(key)) return;
|
||||
seenNotices.add(key);
|
||||
if (page !== "friends") toast(text);
|
||||
}
|
||||
|
||||
/** Alle 3 s auf der Seite Freunde, sonst alle 15 s (Badge, Meldungen) - meldet einen auch als "online". */
|
||||
let socialTick = 0;
|
||||
setInterval(async () => {
|
||||
if ($("app").classList.contains("hidden")) return;
|
||||
if (page !== "friends" && socialTick++ % 5 !== 0) return;
|
||||
try { await loadSocial(); if (page === "friends") drawSocial(); } catch { }
|
||||
}, 3000);
|
||||
|
||||
async function renderFriends(root) {
|
||||
root.replaceChildren(h("h1", {}, "Freunde & Lobby"), h("div", { class: "social", id: "socialRoot" }, h("p", { class: "muted" }, "Laden …")));
|
||||
try { await loadSocial(); }
|
||||
catch (e) { $("socialRoot").replaceChildren(h("p", { class: "error" }, e.message)); return; }
|
||||
if (page === "friends") drawSocial();
|
||||
}
|
||||
|
||||
/** Knopf, der eine Aktion ausfuehrt und danach neu laedt. */
|
||||
function action(label, cls, cmd, args, done) {
|
||||
const b = h("button", { class: "btn small " + (cls || ""), type: "button" }, label);
|
||||
b.addEventListener("click", () => busy(b, async () => {
|
||||
await call(cmd, args || {});
|
||||
if (done) done();
|
||||
await loadSocial();
|
||||
drawSocial();
|
||||
}));
|
||||
return b;
|
||||
}
|
||||
|
||||
function drawSocial() {
|
||||
const root = $("socialRoot");
|
||||
if (!root) return;
|
||||
root.replaceChildren(lobbyCard(), friendsCard());
|
||||
}
|
||||
|
||||
function lobbyCard() {
|
||||
const { lobby, invites } = social.lobby;
|
||||
const card = h("div", { class: "card" });
|
||||
for (const i of invites)
|
||||
card.append(h("div", { class: "invite" }, h("span", {}, h("b", {}, i.owner), " lädt dich in die Lobby ein"),
|
||||
action("Beitreten", "primary", "lobbyJoin", { lobbyId: i.lobbyId }), action("Ablehnen", "", "lobbyDecline", { lobbyId: i.lobbyId })));
|
||||
|
||||
if (!lobby) {
|
||||
card.append(h("h3", {}, "Lobby"),
|
||||
h("p", { class: "muted small" }, "Erstelle eine Lobby und lade Freunde ein. Der Server läuft auf dem BSO-Relay – niemand muss etwas hosten oder Ports freigeben."),
|
||||
h("div", { class: "row" }, action("Lobby erstellen", "primary", "lobbyCreate")));
|
||||
return card;
|
||||
}
|
||||
|
||||
const play = h("button", { class: "btn primary play", type: "button" }, "SPIELEN");
|
||||
play.disabled = !lobby.serverOnline;
|
||||
play.addEventListener("click", () => busy(play, async () => { await call("playLobby"); toast("Bus-Simulator 2012 startet und verbindet mit der Lobby …"); }));
|
||||
|
||||
const state = h("div", { class: "server-state" },
|
||||
h("span", { class: "dot " + (lobby.serverOnline ? "ok" : "bad") }),
|
||||
h("span", { style: "flex:1" }, lobby.serverOnline ? "Server läuft" : lobby.isOwner ? "Server aus – starte ihn, dann können alle spielen" : `Server aus – warte, bis ${lobby.owner} ihn startet`),
|
||||
lobby.isOwner ? (lobby.serverOnline ? action("Server stoppen", "", "lobbyStop") : action("Server starten", "primary", "lobbyStart")) : null);
|
||||
|
||||
const members = h("div", { class: "people" }, ...lobby.members.map((m) => h("div", { class: "person" },
|
||||
h("span", { class: "dot " + (m.online ? "ok" : "") }),
|
||||
h("span", { class: "name" }, m.username),
|
||||
m.isOwner ? h("span", { class: "crown", title: "Ersteller" }, "★") : null,
|
||||
lobby.isOwner && !m.isOwner ? action("Entfernen", "ghost", "lobbyKick", { accountId: m.accountId }) : null)));
|
||||
|
||||
card.append(h("h3", {}, lobby.isOwner ? "Deine Lobby" : `Lobby von ${lobby.owner}`), state,
|
||||
h("div", { class: "section-title" }, `Mitglieder (${lobby.members.length})`), members,
|
||||
lobby.invited.length ? h("p", { class: "muted small" }, `${lobby.invited.length} Einladung(en) offen`) : null,
|
||||
h("div", { class: "row", style: "justify-content:space-between;margin-top:6px" }, play,
|
||||
action(lobby.isOwner ? "Lobby auflösen" : "Lobby verlassen", "danger", "lobbyLeave")));
|
||||
return card;
|
||||
}
|
||||
|
||||
function friendsCard() {
|
||||
const { friends, incoming, outgoing } = social.friends;
|
||||
const lobby = social.lobby.lobby;
|
||||
const name = h("input", { placeholder: "Benutzername", maxlength: "32", spellcheck: "false" });
|
||||
const add = h("button", { class: "btn small primary", type: "button" }, "Hinzufügen");
|
||||
const send = () => busy(add, async () => {
|
||||
if (!name.value.trim()) return;
|
||||
await call("friendRequest", { username: name.value.trim() });
|
||||
toast("Anfrage an " + name.value.trim() + " geschickt");
|
||||
name.value = "";
|
||||
await loadSocial();
|
||||
drawSocial();
|
||||
});
|
||||
add.addEventListener("click", send);
|
||||
name.addEventListener("keydown", (e) => { if (e.key === "Enter") send(); });
|
||||
|
||||
const card = h("div", { class: "card" }, h("h3", {}, "Freunde"), h("div", { class: "row" }, name, add));
|
||||
if (incoming.length)
|
||||
card.append(h("div", { class: "section-title" }, "Anfragen"), h("div", { class: "people" }, ...incoming.map((r) => h("div", { class: "person" },
|
||||
h("span", { class: "name" }, r.username), action("Annehmen", "primary", "friendAccept", { id: r.id }), action("Ablehnen", "", "friendRemove", { id: r.id })))));
|
||||
|
||||
card.append(h("div", { class: "section-title" }, `Freunde (${friends.filter((f) => f.online).length} online)`));
|
||||
if (!friends.length) card.append(h("p", { class: "muted small" }, "Noch keine Freunde – füge jemanden über seinen BSO-Benutzernamen hinzu."));
|
||||
card.append(h("div", { class: "people" }, ...friends.map((f) => {
|
||||
const inLobby = lobby && lobby.members.some((m) => m.accountId === f.accountId);
|
||||
const invited = lobby && lobby.invited.includes(f.accountId);
|
||||
const remove = h("button", { class: "x", type: "button", title: "Freund entfernen" }, "");
|
||||
remove.addEventListener("click", () => { if (confirm(`${f.username} als Freund entfernen?`)) busy(remove, async () => { await call("friendRemove", { id: f.id }); await loadSocial(); drawSocial(); }); });
|
||||
return h("div", { class: "person" },
|
||||
h("span", { class: "dot " + (f.online ? "ok" : "") }), h("span", { class: "name" }, f.username),
|
||||
lobby && lobby.isOwner && !inLobby ? (invited ? h("span", { class: "muted small" }, "eingeladen") : action("Einladen", "", "lobbyInvite", { accountId: f.accountId })) : null,
|
||||
inLobby ? h("span", { class: "muted small" }, "in der Lobby") : null,
|
||||
remove);
|
||||
})));
|
||||
|
||||
if (outgoing.length)
|
||||
card.append(h("div", { class: "section-title" }, "Gesendet"), h("div", { class: "people" }, ...outgoing.map((r) => h("div", { class: "person" },
|
||||
h("span", { class: "name" }, r.username), h("span", { class: "muted small" }, "wartet …"), action("Zurückziehen", "ghost", "friendRemove", { id: r.id })))));
|
||||
return card;
|
||||
}
|
||||
|
||||
async function renderNews(root) {
|
||||
@@ -362,4 +438,4 @@ async function renderSettings(root) {
|
||||
const me = await call("me");
|
||||
if (me.loggedIn) showApp(me.username);
|
||||
else showAuth();
|
||||
})();
|
||||
})();
|
||||
Reference in New Issue
Block a user