Phone runtime (2a): connected directory + dial resolution
GET /playthroughs/:id/phone returns the directory of the phone node the current node is wired to; POST /playthroughs/:id/dial resolves a number to connect (advance to the wired dialogue), voicemail (known contact, no line here), or not-in-service. Barricelli's note-board is wired to the phone node and Glitch Hunter's decline returns to it. Frontend (inventory on the board) is 2b. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,11 @@
|
||||
"key": "report_back",
|
||||
"label": "Call Glitch Hunter",
|
||||
"to": "hunter-intro"
|
||||
},
|
||||
{
|
||||
"key": "phone",
|
||||
"label": "Phone",
|
||||
"to": "phone"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -174,7 +179,7 @@
|
||||
{
|
||||
"key": "decline",
|
||||
"label": "Back",
|
||||
"to": "dobby-tasks"
|
||||
"to": "note-board"
|
||||
}
|
||||
],
|
||||
"utterances": [
|
||||
|
||||
@@ -516,6 +516,15 @@ 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) }
|
||||
})
|
||||
// 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))) }
|
||||
catch (error) { next(error) }
|
||||
})
|
||||
app.post('/api/playthroughs/:id/dial', async (req, res, next) => {
|
||||
try { res.json(await narrative.dial(String(req.params.id), String(req.body?.number || ''))) }
|
||||
catch (error) { next(error) }
|
||||
})
|
||||
// Dev teleport: jump the playthrough to an explicit story node. Powers /node/:id.
|
||||
app.post('/api/playthroughs/:id/goto', async (req, res, next) => {
|
||||
try {
|
||||
|
||||
@@ -59,6 +59,8 @@ 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 }>
|
||||
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 }>
|
||||
listMysteries(): Promise<MysterySummary[]>
|
||||
listPlayableMysteries(): Promise<{ slug: string; title: string }[]>
|
||||
@@ -280,6 +282,46 @@ export function createNarrativeRepository(pool: Pool, objectStorage: ObjectStora
|
||||
return { ok: true, earned: (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) {
|
||||
const pt = (await pool.query<{ current_node_id: string | null }>('SELECT current_node_id FROM osint.playthroughs WHERE id=$1', [playthroughId])).rows[0]
|
||||
if (!pt?.current_node_id) return { available: false, numbers: [] }
|
||||
const phoneNode = (await pool.query<{ id: string }>(
|
||||
`SELECT pn.id FROM osint.story_node_terminals t JOIN osint.story_nodes pn ON pn.id=t.to_node_id
|
||||
WHERE t.parent_node_id=$1 AND pn.node_type='phone' LIMIT 1`, [pt.current_node_id])).rows[0]
|
||||
if (!phoneNode) return { available: true, numbers: [] }
|
||||
const dir = (await pool.query<{ number: string; name: string }>(
|
||||
`SELECT npc.phone_number AS number, npc.name FROM osint.story_node_terminals t JOIN osint.npcs npc ON npc.id=t.npc_id
|
||||
WHERE t.parent_node_id=$1 AND npc.phone_number IS NOT NULL ORDER BY t.sort_order`, [phoneNode.id])).rows
|
||||
return { available: true, numbers: dir }
|
||||
},
|
||||
|
||||
// Resolve a dialed number: connect (advance to the wired dialogue), voicemail (a
|
||||
// known contact with no line here), or not-in-service (no such number).
|
||||
async dial(playthroughId, rawNumber) {
|
||||
const number = rawNumber.replace(/\D/g, '')
|
||||
if (!number) return { outcome: 'unknown' }
|
||||
const pt = (await pool.query<{ current_node_id: string | null }>('SELECT current_node_id FROM osint.playthroughs WHERE id=$1', [playthroughId])).rows[0]
|
||||
if (!pt?.current_node_id) return { outcome: 'unknown' }
|
||||
const phoneNode = (await pool.query<{ id: string }>(
|
||||
`SELECT pn.id FROM osint.story_node_terminals t JOIN osint.story_nodes pn ON pn.id=t.to_node_id
|
||||
WHERE t.parent_node_id=$1 AND pn.node_type='phone' LIMIT 1`, [pt.current_node_id])).rows[0]
|
||||
if (phoneNode) {
|
||||
const term = (await pool.query<{ to_node_id: string | null; name: string }>(
|
||||
`SELECT t.to_node_id, npc.name FROM osint.story_node_terminals t JOIN osint.npcs npc ON npc.id=t.npc_id
|
||||
WHERE t.parent_node_id=$1 AND regexp_replace(npc.phone_number,'\\D','','g')=$2 LIMIT 1`, [phoneNode.id, number])).rows[0]
|
||||
if (term?.to_node_id) {
|
||||
await pool.query('UPDATE osint.playthroughs SET current_node_id=$2,current_level_id=NULL,updated_at=NOW() WHERE id=$1', [playthroughId, term.to_node_id])
|
||||
const state = await stateForPlaythrough(playthroughId)
|
||||
return { outcome: 'connect', name: term.name, state: state ?? undefined }
|
||||
}
|
||||
}
|
||||
const npc = (await pool.query<{ name: string }>(
|
||||
`SELECT name FROM osint.npcs WHERE regexp_replace(phone_number,'\\D','','g')=$1 AND mystery_id IS NULL LIMIT 1`, [number])).rows[0]
|
||||
return npc ? { outcome: 'voicemail', name: npc.name } : { outcome: 'unknown' }
|
||||
},
|
||||
|
||||
async listAchievements(playthroughId) {
|
||||
if (!(await pool.query('SELECT 1 FROM osint.playthroughs WHERE id=$1', [playthroughId])).rowCount) return null
|
||||
const rows = (await pool.query<{ flag_key: string }>('SELECT flag_key FROM osint.achievements WHERE playthrough_id=$1 ORDER BY flag_key', [playthroughId])).rows
|
||||
|
||||
Reference in New Issue
Block a user