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
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}`);
|