diff --git a/html/tavern.html b/html/tavern.html
index c96827f..9353214 100644
--- a/html/tavern.html
+++ b/html/tavern.html
@@ -323,7 +323,10 @@
// ---- Chat --------------------------------------------------------------------
socket.on('chat_broadcast', function (m) {
- if (!m || !state.channel || m.scope !== state.channel) return;
+ if (!m) return;
+ // Accept the unified GLOBAL chat (no scope / 'global') as well as this tavern's room.
+ var isGlobal = !m.scope || m.scope === 'global';
+ if (!isGlobal && m.scope !== state.channel) return;
addChatLine(m.username, m.message);
// Show a speech bubble above the speaker's avatar.
if (tavernLife && m.socketId) {
@@ -335,6 +338,16 @@
socket.on('tavern_notice', function (n) {
if (n && n.message) addSystemLine(n.message);
});
+ // Global chat backlog sent on join — render it so the tavern shows history, not a blank log.
+ socket.on('chat_history', function (payload) {
+ var msgs = payload && payload.messages;
+ if (!msgs || !msgs.length) return;
+ for (var i = 0; i < msgs.length; i++) {
+ var mm = msgs[i] || {};
+ var uname = mm.username || mm.playerId || (mm.socketId ? String(mm.socketId).slice(0, 6) : '???');
+ addChatLine(uname, mm.message || '');
+ }
+ });
function addSystemLine(text) {
var line = document.createElement('div');
line.className = 'cline csys';
diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js
index e17b993..68dd749 100644
--- a/src/network/socketHandlers.js
+++ b/src/network/socketHandlers.js
@@ -209,7 +209,10 @@ class SocketHandlers {
debugManager: this.debugManager,
roomData: tavernRoomData,
roomUrl: tavernRoomData ? tavernRoomUrl : null,
- entitlementProvider: async (socket) => this._entitlementsForSocket(socket)
+ entitlementProvider: async (socket) => this._entitlementsForSocket(socket),
+ // Share the lobby's global chat provider so the tavern joins one global chat with
+ // persistent history (and nostr fan-out when enabled), instead of ephemeral room chat.
+ globalChatProvider: this.chatHandler.chatProvider
});
this.tavernManager.initialize();
// Initialize match mode queue, scheduler, and manager. Inert unless MATCH_ENABLED=true.
diff --git a/src/network/tavernManager.js b/src/network/tavernManager.js
index 59b865b..fd6b649 100644
--- a/src/network/tavernManager.js
+++ b/src/network/tavernManager.js
@@ -23,7 +23,7 @@ function escapeHtml(s) {
* is broadcast to occupants once per server tick (clients render from these snapshots).
*/
class TavernManager {
- constructor({ io, debugManager, roomId = 'main', tickMs = null, roomData = null, roomUrl = null, entitlementProvider = null } = {}) {
+ constructor({ io, debugManager, roomId = 'main', tickMs = null, roomData = null, roomUrl = null, entitlementProvider = null, globalChatProvider = null } = {}) {
this.io = io;
this.debugManager = debugManager;
this.enabled = process.env.TAVERN_ENABLED === 'true';
@@ -42,8 +42,10 @@ class TavernManager {
this.room = new Room({ id: this.roomId, type: 'tavern', roomData: roomData, roomUrl: roomUrl });
- // Tavern chat is ephemeral (no history) and delivered only to the tavern room, via the
- // same ChatProvider seam the global chat uses.
+ // Chat: prefer the shared GLOBAL chat provider (persistent history + cross-page/relay
+ // fan-out) so the tavern participates in one global chat with backlog. When it isn't
+ // injected, fall back to an ephemeral tavern-scoped provider (previous behavior).
+ this.globalChatProvider = globalChatProvider;
this.chatProvider = new SocketChatProvider({ io: this.io, debugManager: this.debugManager });
}
@@ -125,6 +127,13 @@ class TavernManager {
socket.join(this.channel);
// Full state (map + occupants) to the joiner; the next tick shows the arrival to others.
socket.emit('tavern_joined', { you: socket.id, state: this.room.fullState() });
+
+ // Send recent GLOBAL chat backlog so the tavern shows history on arrival (unified chat).
+ if (this.globalChatProvider && typeof this.globalChatProvider.getHistory === 'function') {
+ this.globalChatProvider.getHistory({ scope: 'global', limit: 50 })
+ .then(messages => { if (messages && messages.length) socket.emit('chat_history', { messages }); })
+ .catch(err => { if (this.debugManager?.CONSOLE_LOGGING) console.error('[Tavern] history load failed:', err.message); });
+ }
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`[Tavern] +${String(socket.id).slice(0, 6)} (${this.room.size} present)`);
}
@@ -165,9 +174,13 @@ class TavernManager {
this._lastChatAt.set(socket.id, now);
const username = occ.name || String(socket.id).slice(0, 6);
- // Escape here (delivery is trusted-escaped, rendered as HTML on the client).
- this.chatProvider.publish({
- scope: this.channel,
+ // Escape here (delivery is trusted-escaped, rendered as HTML on the client). Route to the
+ // global chat (persisted + broadcast to everyone + relayed over nostr when enabled) so the
+ // tavern shares one global conversation; fall back to tavern-scoped if no global provider.
+ const provider = this.globalChatProvider || this.chatProvider;
+ const scope = this.globalChatProvider ? 'global' : this.channel;
+ provider.publish({
+ scope,
username,
text: escapeHtml(text),
ts: now,
diff --git a/test/tavernManager.test.js b/test/tavernManager.test.js
index 7b8480e..1111300 100644
--- a/test/tavernManager.test.js
+++ b/test/tavernManager.test.js
@@ -227,5 +227,51 @@ describe('TavernManager', () => {
mgr.shutdown();
expect(mgr._tickInterval).toBeNull();
});
+
+ describe('global chat wiring', () => {
+ function makeGlobalProvider() {
+ return {
+ published: [],
+ publish(msg) { this.published.push(msg); return Promise.resolve(); },
+ getHistory() { return Promise.resolve([{ username: 'bob', message: 'gm all' }]); }
+ };
+ }
+
+ test('join sends the global chat backlog to the joiner', async () => {
+ const io = makeFakeIo();
+ const gcp = makeGlobalProvider();
+ const mgr = new TavernManager({ io, globalChatProvider: gcp });
+ const s = makeFakeSocket('a', io);
+ await mgr.join(s, { name: 'Alice' });
+ await new Promise(r => setImmediate(r)); // let the history promise resolve
+ const hist = s.emitted.find(e => e.event === 'chat_history');
+ expect(hist).toBeDefined();
+ expect(hist.payload.messages[0].message).toBe('gm all');
+ });
+
+ test('chat routes to the GLOBAL scope through the shared provider', async () => {
+ const io = makeFakeIo();
+ const gcp = makeGlobalProvider();
+ const mgr = new TavernManager({ io, globalChatProvider: gcp });
+ const s = makeFakeSocket('a', io);
+ await mgr.join(s, { name: 'Alice' });
+ mgr.chat(s, { text: 'hello world' });
+ const sent = gcp.published.find(m => m.text && m.text.indexOf('hello') !== -1);
+ expect(sent).toBeDefined();
+ expect(sent.scope).toBe('global');
+ });
+
+ test('without a global provider, chat stays tavern-scoped (legacy behavior)', async () => {
+ const io = makeFakeIo();
+ const mgr = new TavernManager({ io }); // no globalChatProvider
+ const s = makeFakeSocket('a', io);
+ await mgr.join(s, { name: 'Alice' });
+ io.emitted.length = 0;
+ mgr.chat(s, { text: 'local only' });
+ const bc = io.emitted.find(e => e.event === 'chat_broadcast');
+ expect(bc).toBeDefined();
+ expect(bc.channel).toBe('tavern:main'); // room-scoped, not global
+ });
+ });
});
});