Phone directory foundation: NPC contacts + phone node + terminal→NPC
NPCs gain phone_number and email (the phone book). A new 'phone' node type holds a directory whose terminals each bind to an NPC via npc_id (the callee reached by dialing that number) and wire to the dialogue that plays on connect. Migration 029 adds the columns/type; repositories and admin API thread contacts and the per-terminal NPC binding. Authoring foundation only — the dial runtime and tools sidebar come next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import type { ObjectStorage } from './objectStorage.js'
|
||||
export type UploadedFile = { buffer: Buffer; originalname: string; mimetype: string; size: number }
|
||||
export type AssetDto = { id: string; originalName: string; mimeType: string; byteSize: number; url: string }
|
||||
export type PoseDto = { poseKey: string; assetId: string; url: string }
|
||||
export type NpcDto = { id: string; key: string; name: string; role: string; defaultPose: string | null; poses: PoseDto[]; inUse: boolean }
|
||||
export type NpcDto = { id: string; key: string; name: string; role: string; defaultPose: string | null; phoneNumber: string | null; email: string | null; poses: PoseDto[]; inUse: boolean }
|
||||
export type MysterySummary = { id: string; slug: string; title: string; nodes: number }
|
||||
|
||||
export type PlaythroughSummary = { id: string; mysterySlug: string; levelSlug: string | null; status: 'active' | 'finished' }
|
||||
@@ -58,8 +58,8 @@ export interface NarrativeRepository {
|
||||
listAssets(): Promise<AssetDto[]>
|
||||
deleteAsset(id: string): Promise<'deleted' | 'in_use' | 'not_found'>
|
||||
listNpcs(): Promise<NpcDto[]>
|
||||
createNpc(input: { key: string; name: string; role?: string; defaultPose?: string | null }): Promise<NpcDto>
|
||||
updateNpc(id: string, input: { name?: string; role?: string; defaultPose?: string | null }): Promise<NpcDto | null>
|
||||
createNpc(input: { key: string; name: string; role?: string; defaultPose?: string | null; phoneNumber?: string | null; email?: string | null }): Promise<NpcDto>
|
||||
updateNpc(id: string, input: { name?: string; role?: string; defaultPose?: string | null; phoneNumber?: string | null; email?: string | null }): Promise<NpcDto | null>
|
||||
deleteNpc(id: string): Promise<'deleted' | 'in_use' | 'not_found'>
|
||||
addPose(npcId: string, poseKey: string, file: UploadedFile): Promise<NpcDto | null>
|
||||
deletePose(npcId: string, poseKey: string): Promise<NpcDto | null>
|
||||
@@ -173,8 +173,8 @@ export function createNarrativeRepository(pool: Pool, objectStorage: ObjectStora
|
||||
}
|
||||
|
||||
async function loadNpc(id: string): Promise<NpcDto | null> {
|
||||
const npc = (await pool.query<{ id: string; npc_key: string; name: string; role: string; default_pose_key: string | null }>(
|
||||
'SELECT id,npc_key,name,role,default_pose_key FROM osint.npcs WHERE id=$1 AND mystery_id IS NULL', [id])).rows[0]
|
||||
const npc = (await pool.query<{ id: string; npc_key: string; name: string; role: string; default_pose_key: string | null; phone_number: string | null; email: string | null }>(
|
||||
'SELECT id,npc_key,name,role,default_pose_key,phone_number,email FROM osint.npcs WHERE id=$1 AND mystery_id IS NULL', [id])).rows[0]
|
||||
if (!npc) return null
|
||||
const [poses, usage] = await Promise.all([
|
||||
pool.query<{ pose_key: string; asset_id: string }>('SELECT pose_key,asset_id FROM osint.npc_poses WHERE npc_id=$1 AND asset_id IS NOT NULL ORDER BY pose_key', [id]),
|
||||
@@ -182,6 +182,7 @@ export function createNarrativeRepository(pool: Pool, objectStorage: ObjectStora
|
||||
])
|
||||
return {
|
||||
id: npc.id, key: npc.npc_key, name: npc.name, role: npc.role, defaultPose: npc.default_pose_key,
|
||||
phoneNumber: npc.phone_number, email: npc.email,
|
||||
poses: poses.rows.map(pose => ({ poseKey: pose.pose_key, assetId: pose.asset_id, url: `/api/assets/${pose.asset_id}` })),
|
||||
inUse: Number(usage.rows[0].count) > 0,
|
||||
}
|
||||
@@ -372,17 +373,19 @@ export function createNarrativeRepository(pool: Pool, objectStorage: ObjectStora
|
||||
const key = input.key.trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '')
|
||||
if (!key) throw new Error('An NPC key is required')
|
||||
const id = randomUUID()
|
||||
await pool.query('INSERT INTO osint.npcs (id,mystery_id,npc_key,name,role,default_pose_key) VALUES ($1,NULL,$2,$3,$4,$5)',
|
||||
[id, key, input.name.trim() || key, input.role?.trim() || '', input.defaultPose || null])
|
||||
await pool.query('INSERT INTO osint.npcs (id,mystery_id,npc_key,name,role,default_pose_key,phone_number,email) VALUES ($1,NULL,$2,$3,$4,$5,$6,$7)',
|
||||
[id, key, input.name.trim() || key, input.role?.trim() || '', input.defaultPose || null, input.phoneNumber?.trim() || null, input.email?.trim() || null])
|
||||
return (await loadNpc(id))!
|
||||
},
|
||||
|
||||
async updateNpc(id, input) {
|
||||
const existing = await loadNpc(id)
|
||||
if (!existing) return null
|
||||
await pool.query('UPDATE osint.npcs SET name=$2,role=$3,default_pose_key=$4 WHERE id=$1 AND mystery_id IS NULL', [
|
||||
await pool.query('UPDATE osint.npcs SET name=$2,role=$3,default_pose_key=$4,phone_number=$5,email=$6 WHERE id=$1 AND mystery_id IS NULL', [
|
||||
id, input.name?.trim() ?? existing.name, input.role?.trim() ?? existing.role,
|
||||
input.defaultPose === undefined ? existing.defaultPose : (input.defaultPose || null),
|
||||
input.phoneNumber === undefined ? existing.phoneNumber : (input.phoneNumber?.trim() || null),
|
||||
input.email === undefined ? existing.email : (input.email?.trim() || null),
|
||||
])
|
||||
return loadNpc(id)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user