You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

88 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Build content/index.json from individual mission files.
* Usage: node content/build-index.mjs
*/
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const missionsDir = join(__dirname, 'missions');
const outputPath = join(__dirname, 'index.json');
// Read all mission JSON files
const files = readdirSync(missionsDir)
.filter(f => f.endsWith('.json'))
.sort(); // B1..B12, V1..V6 (lexicographic works for our IDs)
console.log(`Found ${files.length} mission files.`);
const missions = [];
for (const file of files) {
const filePath = join(missionsDir, file);
const raw = readFileSync(filePath, 'utf-8');
const mission = JSON.parse(raw);
missions.push(mission);
console.log(` ✓ ${mission.id}: ${mission.titleIt} (${mission.chunks.length} chunks, ${mission.dialogues.length} dialogues)`);
}
// Validate basic structure
for (const m of missions) {
if (!m.id || !m.trail || !m.cefr || !m.titleIt || !m.titlePt) {
throw new Error(`Mission ${m.id || '?'} missing required fields`);
}
if (!Array.isArray(m.canDos) || m.canDos.length < 2) {
throw new Error(`Mission ${m.id}: needs >=2 canDos`);
}
if (!Array.isArray(m.chunks) || m.chunks.length < 12) {
throw new Error(`Mission ${m.id}: needs >=12 chunks, got ${m.chunks?.length}`);
}
if (!Array.isArray(m.dialogues) || m.dialogues.length !== 2) {
throw new Error(`Mission ${m.id}: needs exactly 2 dialogues`);
}
for (let i = 0; i < m.dialogues.length; i++) {
const d = m.dialogues[i];
if (d.length < 6 || d.length > 10) {
throw new Error(`Mission ${m.id} dialogue ${i+1}: needs 6-10 lines, got ${d.length}`);
}
}
if (!m.roleplay || !m.roleplay.settingIt || !m.roleplay.openerIt) {
throw new Error(`Mission ${m.id}: incomplete roleplay`);
}
if (!Array.isArray(m.survivalChecklist) || m.survivalChecklist.length < 3) {
throw new Error(`Mission ${m.id}: needs >=3 survivalChecklist items`);
}
// Check each chunk has audioKey
for (const c of m.chunks) {
if (!c.it || !c.pt || !c.audioKey) {
throw new Error(`Mission ${m.id}: chunk missing it/pt/audioKey`);
}
}
// Check each dialogue line has audioKey
for (const d of m.dialogues) {
for (const line of d) {
if (!line.speaker || !line.it || !line.pt || !line.audioKey) {
throw new Error(`Mission ${m.id}: dialogue line missing fields`);
}
}
}
}
const bundle = {
schemaVersion: 1,
missions
};
writeFileSync(outputPath, JSON.stringify(bundle, null, 2), 'utf-8');
console.log(`\n✅ Wrote ${outputPath} with ${missions.length} missions.`);
// Summary stats
const totalChunks = missions.reduce((s, m) => s + m.chunks.length, 0);
const totalDialogues = missions.reduce((s, m) => s + m.dialogues.length, 0);
const totalLines = missions.reduce((s, m) => s + m.dialogues.reduce((a, d) => a + d.length, 0), 0);
console.log(` Total chunks: ${totalChunks}`);
console.log(` Total dialogues: ${totalDialogues}`);
console.log(` Total dialogue lines: ${totalLines}`);