Field notebook: capture NPC lines, tear pages onto the board
A dialogue line gains a "✎ Note this" capture that saves it to a per-playthrough notebook (migration 035 + notebook endpoints). The inventory notebook lists captured pages in a handwriting font (Google "Reenie Beanie") and "✂ Tear to board" drops a page onto the current board as a note exhibit (reusing addNote), then removes the page. Board notes render handwritten too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -516,6 +516,23 @@ app.post('/api/playthroughs/:id/utterances/:uid/reach', async (req, res, next) =
|
||||
result.ok ? res.json({ earned: result.earned ?? false }) : res.status(404).json({ error: 'Not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
// Field notebook: capture NPC lines during play, list them, and remove (on tear/discard).
|
||||
app.get('/api/playthroughs/:id/notebook', async (req, res, next) => {
|
||||
try { res.json(await narrative.notebookPages(String(req.params.id))) }
|
||||
catch (error) { next(error) }
|
||||
})
|
||||
app.post('/api/playthroughs/:id/notebook', async (req, res, next) => {
|
||||
try {
|
||||
const page = await narrative.addNotebookPage(String(req.params.id), String(req.body?.text || ''), req.body?.utteranceId ? String(req.body.utteranceId) : null)
|
||||
page ? res.status(201).json(page) : res.status(400).json({ error: 'Empty text or unknown playthrough' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.delete('/api/playthroughs/:id/notebook/:pageId', async (req, res, next) => {
|
||||
try {
|
||||
const ok = await narrative.removeNotebookPage(String(req.params.id), String(req.params.pageId))
|
||||
ok ? res.json({ ok: true }) : res.status(404).json({ error: 'Not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
// The phone tool: the directory available on the current node, and dialing a number.
|
||||
app.get('/api/playthroughs/:id/phone', async (req, res, next) => {
|
||||
try { res.json(await narrative.phoneDirectory(String(req.params.id))) }
|
||||
|
||||
@@ -59,6 +59,9 @@ export interface NarrativeRepository {
|
||||
listAchievements(playthroughId: string): Promise<string[] | null>
|
||||
awardAchievement(playthroughId: string, flagKey: string, nodeId?: string | null): Promise<{ ok: boolean; earned: boolean; error?: string }>
|
||||
reachUtterance(playthroughId: string, utteranceId: string): Promise<{ ok: boolean; earned?: boolean }>
|
||||
notebookPages(playthroughId: string): Promise<{ id: string; text: string; createdAt: string }[]>
|
||||
addNotebookPage(playthroughId: string, text: string, sourceUtteranceId?: string | null): Promise<{ id: string; text: string } | null>
|
||||
removeNotebookPage(playthroughId: string, pageId: string): Promise<boolean>
|
||||
phoneDirectory(playthroughId: string): Promise<{ available: boolean; numbers: { number: string; name: string }[] }>
|
||||
dial(playthroughId: string, number: string): Promise<{ outcome: 'connect' | 'voicemail' | 'unknown'; name?: string; state?: PlaythroughState }>
|
||||
gotoNode(userId: string, playthroughId: string, nodeId: string): Promise<{ ok: boolean; state?: PlaythroughState; error?: string }>
|
||||
@@ -282,6 +285,26 @@ export function createNarrativeRepository(pool: Pool, objectStorage: ObjectStora
|
||||
return { ok: true, earned: (result.rowCount || 0) > 0 }
|
||||
},
|
||||
|
||||
// Field notebook: lines the player captured from NPCs during this playthrough.
|
||||
async notebookPages(playthroughId) {
|
||||
const rows = (await pool.query<{ id: string; text: string; created_at: Date }>(
|
||||
'SELECT id,text,created_at FROM osint.notebook_pages WHERE playthrough_id=$1 ORDER BY created_at', [playthroughId])).rows
|
||||
return rows.map(row => ({ id: row.id, text: row.text, createdAt: row.created_at.toISOString() }))
|
||||
},
|
||||
async addNotebookPage(playthroughId, text, sourceUtteranceId) {
|
||||
const clean = text.trim()
|
||||
if (!clean) return null
|
||||
if (!(await pool.query('SELECT 1 FROM osint.playthroughs WHERE id=$1', [playthroughId])).rowCount) return null
|
||||
const row = (await pool.query<{ id: string }>(
|
||||
'INSERT INTO osint.notebook_pages (playthrough_id,text,source_utterance_id) VALUES ($1,$2,$3) RETURNING id',
|
||||
[playthroughId, clean, sourceUtteranceId || null])).rows[0]
|
||||
return { id: row.id, text: clean }
|
||||
},
|
||||
async removeNotebookPage(playthroughId, pageId) {
|
||||
const result = await pool.query('DELETE FROM osint.notebook_pages WHERE id=$1 AND playthrough_id=$2', [pageId, playthroughId])
|
||||
return (result.rowCount || 0) > 0
|
||||
},
|
||||
|
||||
// The phone directory available on the player's current node: the terminals of a
|
||||
// phone node the current node is wired to. No connected phone node => nobody's listed.
|
||||
async phoneDirectory(playthroughId) {
|
||||
|
||||
Reference in New Issue
Block a user