import { join } from "@std/path"; import { ensureDir } from "@std/fs/ensure-dir"; import type { CampaignInfoJson, CampaignInventory, CampainMapPosition, ChallengeJson, InventoryMission, } from "./campaign-types.ts"; import { defaultChallengeModifiers, mergeChallengeModifiers, } from "./campaign-types.ts"; import { writeMissionProgressionMarkdown } from "./mission-markdown.ts"; function defaultLinearMapPositions(n: number): CampainMapPosition[] { const out: CampainMapPosition[] = []; for (let i = 0; i < n; i++) { const child = i < n - 1 ? [i + 1] : []; out.push({ x: i * 140, y: 0, scale: 1, numberPortion: i, letterPortion: "", childNodes: child, }); } return out; } /** Ensure mapPositions length matches mission count; pad with defaults if partial. */ export function resolveMapPositions( inv: CampaignInventory, ): CampainMapPosition[] { const n = inv.missions.length; if (n === 0) return []; const existing = inv.mapPositions ?? []; if (existing.length === 0) { return defaultLinearMapPositions(n); } if (existing.length === n) return existing; if (existing.length > n) return existing.slice(0, n); const base = defaultLinearMapPositions(n); for (let i = 0; i < existing.length; i++) { base[i] = { ...base[i], ...existing[i] }; } return base; } function missionToChallenge(m: InventoryMission): ChallengeJson { const mods = mergeChallengeModifiers( defaultChallengeModifiers(), m.modifiers, ); const chall: ChallengeJson = { name: m.name, songid: m.songid, hash: m.hash ?? "", customDownloadURL: m.customDownloadURL ?? "", characteristic: m.characteristic, difficulty: m.difficulty, modifiers: mods, unlockMap: m.unlockMap ?? false, requirements: m.requirements ?? [], }; if (m.allowStandardLevel !== undefined) { chall.allowStandardLevel = m.allowStandardLevel; } if (m.externalModifiers && Object.keys(m.externalModifiers).length > 0) { chall.externalModifiers = m.externalModifiers; } if (m.unlockableItems && m.unlockableItems.length > 0) { chall.unlockableItems = m.unlockableItems; } if (m.challengeInfo !== undefined) chall.challengeInfo = m.challengeInfo; return chall; } export type GenerateResult = { outDir: string; missionCount: number; markdownPath: string; markdownWarnings: string[]; }; export async function generateCampaignToDist( inv: CampaignInventory, distRoot: string, ): Promise { const outDir = join(distRoot, inv.outputFolderName); await ensureDir(outDir); const sorted = [...inv.missions].sort((a, b) => a.index - b.index); const mapPositions = resolveMapPositions({ ...inv, missions: sorted }); const info: CampaignInfoJson = { name: inv.info.name, desc: inv.info.desc, bigDesc: inv.info.bigDesc, allUnlocked: inv.info.allUnlocked, mapHeight: inv.info.mapHeight, backgroundAlpha: inv.info.backgroundAlpha, lightColor: inv.info.lightColor, unlockGate: inv.info.unlockGate, mapPositions, useStandardLevel: inv.info.useStandardLevel, customMissionLeaderboard: inv.info.customMissionLeaderboard, }; await Deno.writeTextFile( join(outDir, "info.json"), JSON.stringify(info, null, 2) + "\n", ); for (const m of sorted) { const chall = missionToChallenge(m); await Deno.writeTextFile( join(outDir, `${m.index}.json`), JSON.stringify(chall, null, 2) + "\n", ); } const { markdownPath, warnings: markdownWarnings } = await writeMissionProgressionMarkdown(outDir, inv, mapPositions, sorted); return { outDir, missionCount: sorted.length, markdownPath, markdownWarnings, }; }