diff --git a/app.py b/app.py index ea42487..6cf6dc6 100644 --- a/app.py +++ b/app.py @@ -217,6 +217,7 @@ def roster(world: dict, now: float) -> list[dict]: "party": (player["action"] or {}).get("party"), "garrison": player["garrison"], "donated": player["donated"], "look": game.look_view(player), + "seen": int(player["last_active"]), }) return sorted(people, key=lambda p: -p["donated"]) diff --git a/game.py b/game.py index 1fa80af..86cd565 100644 --- a/game.py +++ b/game.py @@ -6084,6 +6084,7 @@ ORDER_RECIPES, ORDER_RAW = _order_sources() # line here (new / balance / fix), and a new release a new entry on top. PATCH_NOTES = [ {"date": "2026-10-26", "title": "Fittings", "notes": [ + ("new", "Guild Roster shows player activity."), ("new", "Make X: \"Make one\" now takes a count (1 by default, up to 1,000), then you go back to what you were doing."), ("new", "Construction › Fittings: ✓ Fitted on the ones the town already has (✓ Paused while the building is damaged)."), ]}, diff --git a/static/app.js b/static/app.js index e7e5c48..81c54d3 100644 --- a/static/app.js +++ b/static/app.js @@ -58,6 +58,11 @@ const serverNow = () => Date.now() / 1000 + skew; const item = (id) => C.items[id] || { name: id, icon: "❔", value: 0 }; const skillOf = (id) => C.skills.find((s) => s.id === id); const playerName = (uid) => S.players.find((p) => p.id === uid)?.name || uid; +// An open tab polls every 10s and a hidden one stops, so a quiet minute means they've gone. +const ONLINE_S = 45; +const online = (p) => p.id === S.uid || serverNow() - p.seen < ONLINE_S; +const presence = (p) => online(p) ? `` : ""; +const lastSeen = (p) => online(p) ? "" : ` · seen ${ago(p.seen)}`; function remember(key, value) { try { @@ -713,7 +718,7 @@ function sidebar() { ${nav("council", "📜", "Council & threats", "", raid ? `` : "", raid ? "A threat is coming" : "")} ${nav("orders", ORDER_ICON(), "Work orders", "", takeable ? `${takeable}` : "", takeable ? `${takeable} for you` : "")} ${nav("board", "💬", "Town board")} - ${nav("roster", "👥", "Guild roster", `${S.players.length}`, "", `${S.players.length} ${S.players.length === 1 ? "adventurer" : "adventurers"}`)} + ${rosterNav()} ${nav("notes", "📰", "Patch notes", "", notesUnread() ? `new` : "", notesUnread() ? `What's new: ${C.patch_notes[0].title}` : "")} ${nav("character", cls.icon, "Character", `${skillOf("combat").icon} ${me.levels.combat}`, "", `Combat level ${me.levels.combat}`)} @@ -2420,12 +2425,18 @@ function joinButton(p) { return ` `; } +function rosterNav() { + const on = S.players.filter(online).length, others = S.players.some((p) => p.id !== S.uid && online(p)); + return nav("roster", "👥", "Guild roster", `${on}/${S.players.length}`, others ? `` : "", `${on} of ${S.players.length} online`); +} + function roster() { - return `
${S.players.map((p) => { + const ordered = [...S.players].sort((a, b) => online(b) - online(a)); // stable: by donations within each + return `
${ordered.map((p) => { const cls = C.classes[p.cls]; const levels = C.skills.filter((s) => !s.class || s.class === p.cls).map((s) => `
${s.icon}${s.name}${p.levels[s.id]}
`).join(""); return `
${cls.icon}
- ${titled(p)} ${cls.name} · total level ${p.total} · ${esc(p.look.home)} + ${presence(p)}${titled(p)} ${cls.name} · total level ${p.total} · ${esc(p.look.home)}${lastSeen(p)}
${p.doing ? `${skillOf(p.skill).icon} ${esc(p.doing)}` : `Idle`}${p.garrison ? ` · On guard` : ""}
${FAVOUR} ${fmt(p.donated)} given to the city
${levels}
@@ -2512,8 +2523,8 @@ function characterSheet(uid) {
${cls.icon}
-

${esc(p.name)}

-
${cls.name} · total level ${total} · ${days === 1 ? "first day" : `${days} days`} in the city
+

${presence(p)}${esc(p.name)}

+
${cls.name} · total level ${total} · ${days === 1 ? "first day" : `${days} days`} in the city${lastSeen(p)}
${p.doing ? `${skillOf(p.skill).icon} ${esc(p.doing)}` : `Idle`}${p.garrison ? ` · On guard` : ""}
diff --git a/static/styles.css b/static/styles.css index 8f04cc6..6c7b2e7 100644 --- a/static/styles.css +++ b/static/styles.css @@ -78,6 +78,7 @@ a { color: var(--xp); } .nav .label { flex: 1; } .nav .meta { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; } .nav .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 6px var(--good); } +.online { display: inline-block; width: 8px; height: 8px; margin-right: 6px; vertical-align: middle; border-radius: 50%; background: var(--good); box-shadow: 0 0 6px var(--good); } .nav .alert { width: 8px; height: 8px; border-radius: 50%; background: var(--bad); } .devbar {