Add authorable per-level briefing checklist
The case briefing showed a wall of body text. Add a checklist of short, player-tickable steps authored per level: a new `checklist` on the level brief (table osint.level_brief_checklist_items), read/written with the board, cloned on template freeze/instantiate, and carried through the mystery importer and the mystery:pull exporter. Player tick state is guidance-only, persisted locally per level rather than on the shared board. Convert the Barricelli inventor-proof brief from prose into a short intro plus a six-step checklist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ export async function clearBoard(client: PoolClient, boardId: string) {
|
||||
await client.query('DELETE FROM osint.board_views WHERE board_id=$1', [boardId])
|
||||
await client.query('DELETE FROM osint.brief_concepts WHERE board_id=$1', [boardId])
|
||||
await client.query('DELETE FROM osint.level_briefs WHERE board_id=$1', [boardId])
|
||||
await client.query('DELETE FROM osint.level_brief_checklist_items WHERE board_id=$1', [boardId])
|
||||
await client.query('DELETE FROM osint.level_goals WHERE board_id=$1', [boardId])
|
||||
await client.query('DELETE FROM osint.evidence_match_rules WHERE board_id=$1', [boardId])
|
||||
await client.query('DELETE FROM osint.metadata_fields WHERE board_id=$1', [boardId])
|
||||
@@ -253,6 +254,8 @@ export async function cloneBoard(client: PoolClient, sourceBoardId: string, targ
|
||||
|
||||
const brief = await client.query<{ body: string }>('SELECT body FROM osint.level_briefs WHERE board_id=$1', [sourceBoardId])
|
||||
if (brief.rows[0]) await client.query('INSERT INTO osint.level_briefs (board_id,body) VALUES ($1,$2)', [targetBoardId, brief.rows[0].body])
|
||||
const checklist = await client.query<{ sort_order: number; text: string }>('SELECT sort_order,text FROM osint.level_brief_checklist_items WHERE board_id=$1 ORDER BY sort_order,id', [sourceBoardId])
|
||||
for (const row of checklist.rows) await client.query('INSERT INTO osint.level_brief_checklist_items (id,board_id,sort_order,text) VALUES ($1,$2,$3,$4)', [randomUUID(), targetBoardId, row.sort_order, row.text])
|
||||
const concepts = await client.query<{
|
||||
id: string; label: string; context_text: string; sort_order: number; expected_party_kind: string | null; resolved_party_exhibit_id: string | null
|
||||
}>('SELECT id,label,context_text,sort_order,expected_party_kind,resolved_party_exhibit_id FROM osint.brief_concepts WHERE board_id=$1 ORDER BY sort_order,id', [sourceBoardId])
|
||||
|
||||
@@ -360,7 +360,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
// a template board that no player has instantiated.
|
||||
async function assembleBoard(level: LevelRow, authorMode = false): Promise<CaseState> {
|
||||
const [exhibitsResult, blocksResult, regionsResult, membershipsResult, connectionsResult, metadataResult, eventEvidenceResult,
|
||||
aliasesResult, partyEvidenceResult, briefResult, conceptsResult, viewsResult, requirementsResult, flagsResult, seenResult] = await Promise.all([
|
||||
aliasesResult, partyEvidenceResult, briefResult, conceptsResult, viewsResult, requirementsResult, flagsResult, seenResult, checklistResult] = await Promise.all([
|
||||
pool.query<ExhibitRow>(`SELECT e.id,e.exhibit_type_id,e.xpos,e.ypos,e.width,e.height,e.rotation,e.z_index,e.hidden,
|
||||
COALESCE(f.title, d.title, n.title, ev.title, p.display_name, claim.statement, '') AS title,
|
||||
COALESCE(f.label_text, n.note_text, ev.narrative_text, p.summary, '') AS content,
|
||||
@@ -419,6 +419,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
'SELECT document_exhibit_id,flag_key FROM osint.document_flag_requirements WHERE board_id=$1 ORDER BY document_exhibit_id,flag_key', [level.board_id]),
|
||||
pool.query<{ flag_key: string }>('SELECT flag_key FROM osint.level_flags WHERE level_id=$1 ORDER BY flag_key', [level.id]),
|
||||
pool.query<{ document_exhibit_id: string }>('SELECT document_exhibit_id FROM osint.level_seen_documents WHERE level_id=$1', [level.id]),
|
||||
pool.query<{ text: string }>('SELECT text FROM osint.level_brief_checklist_items WHERE board_id=$1 ORDER BY sort_order,id', [level.board_id]),
|
||||
])
|
||||
|
||||
const blocks = new Map<string, string[]>()
|
||||
@@ -480,7 +481,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
tagPosition: row.tag_position_percent, tagOffset: row.tag_lateral_offset })),
|
||||
viewport: { x: level.viewport_x, y: level.viewport_y, zoom: level.viewport_zoom }, updatedAt: level.updated_at.toISOString(),
|
||||
views, revision: Number(level.revision),
|
||||
brief: { body: briefResult.rows[0]?.body || '', concepts }, goals, report, levelStatus: level.status, editingAllowed: editingEnabled && authorMode,
|
||||
brief: { body: briefResult.rows[0]?.body || '', concepts, checklist: checklistResult.rows.map(row => row.text) }, goals, report, levelStatus: level.status, editingAllowed: editingEnabled && authorMode,
|
||||
sourceTemplateVersionId: level.source_template_version_id || undefined }
|
||||
return filterLevelVisibility(fullState, flagsResult.rows.map(row => row.flag_key), seenResult.rows.map(row => row.document_exhibit_id), authorMode)
|
||||
}
|
||||
@@ -520,6 +521,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
await client.query('DELETE FROM osint.board_views WHERE board_id=$1', [level.board_id])
|
||||
await client.query('DELETE FROM osint.brief_concepts WHERE board_id=$1', [level.board_id])
|
||||
await client.query('DELETE FROM osint.level_briefs WHERE board_id=$1', [level.board_id])
|
||||
await client.query('DELETE FROM osint.level_brief_checklist_items WHERE board_id=$1', [level.board_id])
|
||||
await client.query('DELETE FROM osint.exhibit_sources WHERE exhibit_id IN (SELECT id FROM osint.exhibits WHERE board_id=$1)', [level.board_id])
|
||||
await client.query('DELETE FROM osint.metadata_fields WHERE board_id=$1', [level.board_id])
|
||||
await client.query('DELETE FROM osint.document_flag_requirements WHERE board_id=$1',[level.board_id])
|
||||
@@ -657,6 +659,8 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
}
|
||||
const brief = state.brief || { body: '', concepts: [] }
|
||||
await client.query('INSERT INTO osint.level_briefs (board_id,body) VALUES ($1,$2)', [level.board_id, brief.body || ''])
|
||||
for (const [sortOrder, item] of (brief.checklist || []).map(text => text.trim()).filter(Boolean).entries())
|
||||
await client.query('INSERT INTO osint.level_brief_checklist_items (id,board_id,sort_order,text) VALUES ($1,$2,$3,$4)', [randomUUID(), level.board_id, sortOrder, item])
|
||||
for (const [sortOrder, concept] of brief.concepts.entries()) {
|
||||
requireUuid(concept.id, 'Brief concept id')
|
||||
if (concept.resolvedPartyExhibitId && !evidenceIds.has(concept.resolvedPartyExhibitId)) throw new Error('Concept resolution references an unknown party')
|
||||
|
||||
Reference in New Issue
Block a user