diff --git a/README.md b/README.md
index 887d2e0..457079f 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,8 @@ The DM presses **⚔ Start fight** in the lobby. Every player with a character j
The bar shows the order, the round and whose turn it is (the token pulses on the map). The DM can overwrite every number, reroll, remove entries, step **◀ / Next turn ▶** and **End fight**. Hidden NPCs are not shown to players. Fight start, joins and the end are logged in the chat feed.
+The top of the lobby is one compact row: small buttons on the left (start fight, previous/next turn, end fight, ▾ to expand players and difficulty), the player bar – replaced by a scrollable initiative bar during a fight – and the lobby settings button. Players only see the name of whoever's turn it is; NPC names are not sent to players otherwise, and token names show on hover, selection or turn. The sheet and the chat can be resized by dragging the thin lines next to them (remembered per browser).
+
### Maps, tokens and line of sight
- **Maps tab** (top navigation): all your maps with preview. Upload PNG, JPEG or WebP (up to 12 MB, 30 maps each; the server checks and re-encodes every image as WebP, max 4096 px per side, into `db/maps/`). **Edit** opens the map editor, **Share** gives other users access by username (like characters: they can put the map on their own table or **Copy** it, but not change it), **Duplicate** copies a map.
diff --git a/backend/app/combat.py b/backend/app/combat.py
index ff81ed9..93a4f2e 100644
--- a/backend/app/combat.py
+++ b/backend/app/combat.py
@@ -58,6 +58,8 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, load_rules):
continue
if toks[e["token"]] and not is_dm:
continue
+ if e["kind"] == "npc" and not is_dm and e["id"] != c.get("current"):
+ e = {**e, "name": ""} # players only learn an NPC's name when it is its turn
out.append({k: e[k] for k in ("id", "kind", "name", "init", "base", "roll") if k in e} | {k: e[k] for k in ("user", "token") if k in e})
cur = c.get("current")
return {"round": c["round"], "current": cur if any(e["id"] == cur for e in out) else None, "entries": out}
diff --git a/backend/app/maps.py b/backend/app/maps.py
index 86151c9..8530b3c 100644
--- a/backend/app/maps.py
+++ b/backend/app/maps.py
@@ -352,6 +352,13 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
LEFT JOIN characters c ON c.id = m.character_id
WHERE t.lobby_id=? ORDER BY t.rowid""", (lobby["id"],)).fetchall()
full = ctx["viewer"] is None
+ turn_tok = "" # players only learn an NPC's name while it is that NPC's turn
+ try:
+ cb = json.loads(lobby["combat"]) if lobby["combat"] else None
+ if cb:
+ turn_tok = next((e.get("token", "") for e in cb["entries"] if e["id"] == cb.get("current")), "")
+ except (ValueError, KeyError, TypeError):
+ pass
out = []
for r in rows:
player = r["user_id"] is not None
@@ -367,6 +374,8 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
name = (json.loads(r["data"]).get("name") if r["data"] else "") or r["username"]
except ValueError:
name = r["username"]
+ if not player and not full and r["id"] != turn_tok:
+ name = ""
pt = ""
src = r["data"] if player else r["ndata"]
if src:
diff --git a/frontend/src/Lobbies.jsx b/frontend/src/Lobbies.jsx
index 3e3de12..18ef5ba 100644
--- a/frontend/src/Lobbies.jsx
+++ b/frontend/src/Lobbies.jsx
@@ -424,7 +424,7 @@ function DiceTray({ rolls }) {
}
// ---- character bar: one card per player, later also for initiative and enemies
-function CharBar({ lobby, sel, onSel, settings }) {
+function CharBar({ lobby, sel, onSel }) {
return (
@@ -448,38 +448,51 @@ function CharBar({ lobby, sel, onSel, settings }) {
)
})}
{lobby.members.length === 0 && Nobody has joined yet – share the join code (⚙ Lobby).}
- {settings}
)
}
-// ---- initiative tracker: the DM starts a fight, players join automatically, NPCs are added from the token menu
-function InitiativeBar({ lobby, onOp, onStart, onEnd, onPick }) {
+// Drag handle that resizes a panel. Reports the new size through onChange, keeps it within min/max. dir = 1: dragging right grows, -1: shrinks.
+function Splitter({ value, onChange, min, max, dir = 1, className = '', label }) {
+ const down = (e) => {
+ e.preventDefault()
+ const x0 = e.clientX, v0 = value
+ document.body.classList.add('resizing')
+ const mv = (ev) => onChange(Math.round(Math.min(max, Math.max(min, v0 + dir * (ev.clientX - x0)))))
+ const up = () => { document.body.classList.remove('resizing'); window.removeEventListener('pointermove', mv); window.removeEventListener('pointerup', up) }
+ window.addEventListener('pointermove', mv); window.addEventListener('pointerup', up)
+ }
+ return
+}
+
+// ---- initiative tracker: compact cards (portrait + number). Players only see the name of whoever's turn it is.
+function InitiativeCards({ lobby, tokens, onOp, onPick }) {
const c = lobby.combat, dm = lobby.is_dm
- if (!c) return dm ? : null
+ const cardPic = (e) => {
+ if (e.kind === 'player') { const m = lobby.members.find((x) => x.username === e.user); return m?.character?.portrait ? `/api/lobbies/${lobby.id}/portrait/${encodeURIComponent(e.user)}?v=${m.character.portrait}` : null }
+ const t = tokens.find((x) => x.id === e.token)
+ return t?.portrait ? `/api/lobbies/${lobby.id}/token-portrait/${t.id}?v=${t.portrait}` : null
+ }
const missing = dm ? lobby.members.filter((m) => m.character && !c.entries.some((e) => e.user === m.username)) : []
return (
-