diff --git a/assets/sci-aquatic-lab.jpg b/assets/sci-aquatic-lab.jpg index 18fed6f..82454c4 100644 Binary files a/assets/sci-aquatic-lab.jpg and b/assets/sci-aquatic-lab.jpg differ diff --git a/assets/sci-botanist.jpg b/assets/sci-botanist.jpg index fd6c7a2..484bf55 100644 Binary files a/assets/sci-botanist.jpg and b/assets/sci-botanist.jpg differ diff --git a/assets/sci-excavation.jpg b/assets/sci-excavation.jpg index 8b7e11b..016e335 100644 Binary files a/assets/sci-excavation.jpg and b/assets/sci-excavation.jpg differ diff --git a/assets/sci-globe-room.jpg b/assets/sci-globe-room.jpg index 09d1beb..2388d4a 100644 Binary files a/assets/sci-globe-room.jpg and b/assets/sci-globe-room.jpg differ diff --git a/assets/sci-growth-chamber.jpg b/assets/sci-growth-chamber.jpg index ca90aa5..1c4faf4 100644 Binary files a/assets/sci-growth-chamber.jpg and b/assets/sci-growth-chamber.jpg differ diff --git a/assets/sci-herbarium.jpg b/assets/sci-herbarium.jpg index c907e10..d4e9ef5 100644 Binary files a/assets/sci-herbarium.jpg and b/assets/sci-herbarium.jpg differ diff --git a/assets/sci-laboratory.jpg b/assets/sci-laboratory.jpg index 01ed10a..dc2dc5a 100644 Binary files a/assets/sci-laboratory.jpg and b/assets/sci-laboratory.jpg differ diff --git a/assets/sci-paleontology.jpg b/assets/sci-paleontology.jpg index 90007b8..34fa100 100644 Binary files a/assets/sci-paleontology.jpg and b/assets/sci-paleontology.jpg differ diff --git a/assets/sci-specimen-hall.jpg b/assets/sci-specimen-hall.jpg index e2a6a47..8ca64c1 100644 Binary files a/assets/sci-specimen-hall.jpg and b/assets/sci-specimen-hall.jpg differ diff --git a/assets/sci-specimen-tanks.jpg b/assets/sci-specimen-tanks.jpg index afc3fdc..e99927c 100644 Binary files a/assets/sci-specimen-tanks.jpg and b/assets/sci-specimen-tanks.jpg differ diff --git a/assets/sci-telescope.jpg b/assets/sci-telescope.jpg index a39801c..978b9e3 100644 Binary files a/assets/sci-telescope.jpg and b/assets/sci-telescope.jpg differ diff --git a/assets/sci-weather-station.jpg b/assets/sci-weather-station.jpg index 065e8a0..e92ef6f 100644 Binary files a/assets/sci-weather-station.jpg and b/assets/sci-weather-station.jpg differ diff --git a/game.html b/game.html index 2c6a6f3..9001ab6 100644 --- a/game.html +++ b/game.html @@ -989,6 +989,16 @@ + +
+ Chronicle updated +
+
{ + if (e.data === 'connected') return + // Don't reload while a modal is open — pick it up when they close + if (this.showModal) return + await Promise.all([this.loadGame(), this.loadItems(), this.loadKeywords()]) + this.liveUpdate = true + setTimeout(() => this.liveUpdate = false, 2500) + } + es.onerror = () => { + es.close() + setTimeout(() => this.connectLive(), 5000) + } + }, + async loadCards() { try { const r = await fetch('/assets/cards.json') diff --git a/server.js b/server.js index 11132e5..15f1679 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,18 @@ const path = require('path') const app = express() const PORT = process.env.PORT || 3000 + +// SSE clients: Map> +const sseClients = new Map() + +function broadcast(slug) { + const bucket = sseClients.get(slug) + if (!bucket) return + const msg = `data: update\n\n` + for (const res of bucket) { + try { res.write(msg) } catch {} + } +} const DB_PATH = path.join(__dirname, 'colony.json') function loadDB() { @@ -59,6 +71,24 @@ function publicGame(g) { } } +// --- Live updates (SSE) --- +app.get('/api/games/:slug/live', (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no', + }) + res.write('data: connected\n\n') + const slug = req.params.slug + if (!sseClients.has(slug)) sseClients.set(slug, new Set()) + sseClients.get(slug).add(res) + req.on('close', () => { + sseClients.get(slug)?.delete(res) + if (sseClients.get(slug)?.size === 0) sseClients.delete(slug) + }) +}) + // --- Games --- app.get('/api/games', (req, res) => { const db = loadDB() @@ -120,7 +150,7 @@ app.put('/api/games/:slug', requirePassword, (req, res) => { game.end_title = end_title || '' game.end_description = end_description || '' game.end_mood = end_mood || '' - saveDB(req.db) + saveDB(req.db); broadcast(req.params.slug) res.json(publicGame(game)) }) @@ -159,7 +189,7 @@ app.post('/api/games/:slug/items', requirePassword, (req, res) => { } req.game.items = req.game.items || [] req.game.items.push(item) - saveDB(req.db) + saveDB(req.db); broadcast(req.params.slug) res.status(201).json(item) }) @@ -171,7 +201,7 @@ app.put('/api/games/:slug/items/:id', requirePassword, (req, res) => { if (!title || !date) return res.status(400).json({ error: 'title, date required' }) const { env, env_layout } = req.body items[idx] = { ...items[idx], title, description: description || '', mood: mood || null, date, end_date: end_date || null, status: status || null, discipline: discipline || null, env: env || null, env_layout: env_layout || null } - saveDB(req.db) + saveDB(req.db); broadcast(req.params.slug) res.json(items[idx]) }) @@ -180,7 +210,7 @@ app.delete('/api/games/:slug/items/:id', requirePassword, (req, res) => { const idx = items.findIndex(i => i.id === req.params.id) if (idx === -1) return res.status(404).json({ error: 'Item not found' }) items.splice(idx, 1) - saveDB(req.db) + saveDB(req.db); broadcast(req.params.slug) res.json({ ok: true }) }) @@ -200,12 +230,12 @@ app.post('/api/games/:slug/keywords', requirePassword, (req, res) => { const existing = req.game.keywords.find(k => k.term.toLowerCase() === term.toLowerCase()) if (existing) { existing.definition = definition || '' - saveDB(req.db) + saveDB(req.db); broadcast(req.params.slug) return res.json(existing) } const kw = { id: newId(), term, definition: definition || '', created_at: now() } req.game.keywords.push(kw) - saveDB(req.db) + saveDB(req.db); broadcast(req.params.slug) res.status(201).json(kw) }) @@ -215,7 +245,7 @@ app.put('/api/games/:slug/keywords/:id', requirePassword, (req, res) => { if (idx === -1) return res.status(404).json({ error: 'Keyword not found' }) const { term, definition } = req.body kws[idx] = { ...kws[idx], term: term || kws[idx].term, definition: definition || '' } - saveDB(req.db) + saveDB(req.db); broadcast(req.params.slug) res.json(kws[idx]) }) @@ -224,7 +254,7 @@ app.delete('/api/games/:slug/keywords/:id', requirePassword, (req, res) => { const idx = kws.findIndex(k => k.id === req.params.id) if (idx === -1) return res.status(404).json({ error: 'Keyword not found' }) kws.splice(idx, 1) - saveDB(req.db) + saveDB(req.db); broadcast(req.params.slug) res.json({ ok: true }) })