"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InkEngine = void 0; exports.compileInkSource = compileInkSource; const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const inkjs_1 = require("inkjs"); const tag_parser_1 = require("../utils/tag-parser"); const { Compiler } = require('inkjs/full'); function compileInkSource(sourcePath, outputPath) { const resolvedSource = path_1.default.resolve(sourcePath); const resolvedOutput = path_1.default.resolve(outputPath); if (!(0, fs_1.existsSync)(resolvedSource)) { throw new Error(`Ink source file not found: ${resolvedSource}`); } const warnings = []; const errors = []; const source = (0, fs_1.readFileSync)(resolvedSource, 'utf8').replace(/^\uFEFF/, ''); const sourceDir = path_1.default.dirname(resolvedSource); const fileHandler = { ResolveInkFilename: (filename) => path_1.default.isAbsolute(filename) ? filename : path_1.default.resolve(sourceDir, filename), LoadInkFileContents: (filename) => (0, fs_1.readFileSync)(path_1.default.isAbsolute(filename) ? filename : path_1.default.resolve(sourceDir, filename), 'utf8') .replace(/^\uFEFF/, ''), }; const compiler = new Compiler(source, { sourceFilename: resolvedSource, fileHandler, errorHandler: (message, type) => { if (type === 1) { warnings.push(message); } else { errors.push(message); } }, }); const story = compiler.Compile(); if (!story || errors.length > 0) { throw new Error(`Ink compilation failed:\n${errors.join('\n')}`); } if (warnings.length > 0) { warnings.forEach((warning) => console.warn(`[ink] ${warning}`)); } (0, fs_1.mkdirSync)(path_1.default.dirname(resolvedOutput), { recursive: true }); (0, fs_1.writeFileSync)(resolvedOutput, story.ToJson(), 'utf8'); return { sourcePath: resolvedSource, outputPath: resolvedOutput, warningCount: warnings.length, }; } class InkEngine { constructor(storyPath) { this.storyPath = storyPath; this.story = null; this.nextTurnId = 1; this.storyJson = null; this.choicePreviewTagKeys = new Set(['action', 'key', 'letter', 'optional', 'gated', 'sort', 'auto']); } isRunning() { if (!this.story) return false; return this.story.canContinue || this.story.currentChoices.length > 0; } newGame() { this.story = this.loadStory(); this.nextTurnId = 1; return this.continueStory(); } chooseChoice(choiceIndex) { if (!this.story) { throw new Error('No active Ink story'); } const choice = this.story.currentChoices.find((item) => item.index === choiceIndex); if (!choice) { throw new Error(`Ink choice ${choiceIndex} is not available`); } this.story.ChooseChoiceIndex(choice.index); return this.continueStory(); } saveGame() { if (!this.story) { throw new Error('No active Ink story to save'); } return JSON.stringify({ inkState: this.story.state.toJson(), nextTurnId: this.nextTurnId, }); } resumeGame(savedState) { this.restoreState(savedState); } loadGame(savedState) { this.restoreState(savedState); return this.continueStory(); } restoreState(savedState) { this.story = this.loadStory(); let inkState = savedState; try { const parsed = JSON.parse(savedState); if (parsed && typeof parsed.inkState === 'string') { inkState = parsed.inkState; if (Number.isInteger(parsed.nextTurnId)) { this.nextTurnId = Math.max(1, parsed.nextTurnId); } } } catch { // Backward compatibility with raw Ink state JSON. } this.story.state.LoadJson(inkState); } loadStory() { const resolvedPath = path_1.default.resolve(this.storyPath); if (!(0, fs_1.existsSync)(resolvedPath)) { throw new Error(`Ink story file not found: ${resolvedPath}`); } this.storyJson = JSON.parse((0, fs_1.readFileSync)(resolvedPath, 'utf8')); return new inkjs_1.Story(this.storyJson); } continueStory() { if (!this.story) { throw new Error('No active Ink story'); } const paragraphs = []; const globalTags = []; const turnTags = []; let pendingParagraphTags = []; while (this.story.canContinue) { const rawText = this.story.Continue(); const text = String(rawText || '').trim(); const tags = (0, tag_parser_1.parseTags)(this.story.currentTags || []); turnTags.push(...tags); tags .filter((tag) => tag.key === 'title' || tag.key === 'author') .forEach((tag) => globalTags.push(tag)); if (text) { const paragraphTags = this.reassignTrailingGlossTags(text, [...pendingParagraphTags, ...tags], paragraphs); pendingParagraphTags = []; paragraphs.push({ text, tags: paragraphTags }); } else { const paragraphTags = this.reassignTrailingGlossTags('', tags, paragraphs); paragraphTags.forEach((tag) => { if (this.isParagraphScopedTag(tag)) { pendingParagraphTags.push(tag); } else { globalTags.push(tag); } }); } } if (pendingParagraphTags.length > 0) { globalTags.push(...pendingParagraphTags); pendingParagraphTags = []; } const choices = this.story.currentChoices.map((choice) => { const tags = this.getChoiceTags(choice); const category = (0, tag_parser_1.getTagValue)(tags, 'action'); const letter = (0, tag_parser_1.getTagValue)(tags, 'letter') || (0, tag_parser_1.getTagValue)(tags, 'key'); return { index: choice.index, text: String(choice.text || '').trim(), tags, category, letter, }; }); const inputMode = choices.length > 0 ? 'choice' : 'end'; const gameState = {}; if (inputMode === 'end') { const errorTag = turnTags.find((tag) => tag.key === 'error'); const scoreTag = turnTags.find((tag) => tag.key === 'score'); if (!errorTag && !scoreTag) { const message = 'Ink story ended without an explicit #score ending tag.'; const generatedErrorTag = { key: 'error', value: message }; globalTags.push(generatedErrorTag); turnTags.push(generatedErrorTag); } const finalErrorTag = turnTags.find((tag) => tag.key === 'error'); const finalScoreTag = turnTags.find((tag) => tag.key === 'score'); if (finalErrorTag) { gameState.endState = { type: 'error', message: finalErrorTag.value || finalErrorTag.param, }; } else if (finalScoreTag) { const numericScore = Number(finalScoreTag?.value); if (Number.isFinite(numericScore)) { gameState.score = numericScore; } gameState.endState = { type: 'intended', message: finalScoreTag.value || finalScoreTag.param, }; } } return { turnId: this.nextTurnId++, paragraphs, choices, inputMode, globalTags: globalTags.length > 0 ? globalTags : undefined, gameState: Object.keys(gameState).length > 0 ? gameState : undefined, }; } isParagraphScopedTag(tag) { const key = String(tag?.key || '').toLowerCase(); return ['chapter', 'heading', 'section', 'textblock', 'image', 'music', 'sfx', 'sound', 'audio', 'gloss', 'tts'] .includes(key) || key.startsWith('tts-'); } reassignTrailingGlossTags(text, tags, paragraphs) { if (!Array.isArray(tags) || tags.length === 0) return []; const previous = paragraphs.length > 0 ? paragraphs[paragraphs.length - 1] : null; if (!previous) return tags; const currentText = this.normalizeGlossMatchText(text); const previousText = this.normalizeGlossMatchText(previous.text); const remainingTags = []; tags.forEach((tag) => { if (tag.key === 'tts' || tag.key.startsWith('tts-')) { if (!currentText) { previous.tags.push(tag); } else { remainingTags.push(tag); } return; } if (tag.key !== 'gloss') { remainingTags.push(tag); return; } const term = this.normalizeGlossMatchText(tag.value || ''); if (!term) { remainingTags.push(tag); return; } if (!currentText) { previous.tags.push(tag); remainingTags.push(tag); return; } const matchesCurrent = currentText.includes(term); const matchesPrevious = previousText.includes(term); if (!matchesCurrent && matchesPrevious) { previous.tags.push(tag); } else { remainingTags.push(tag); } }); return remainingTags; } normalizeGlossMatchText(value) { return String(value || '') .normalize('NFC') .toLocaleLowerCase('de-DE') .replace(/[.,;:!?()[\]{}"'„“”‚‘’»«]/g, ' ') .replace(/\s+/g, ' ') .trim(); } getChoiceTags(choice) { const directTags = (0, tag_parser_1.parseTags)(choice?.tags || []); const previewTags = this.extractChoicePreviewTags(choice); const merged = new Map(); [...previewTags, ...directTags].forEach((tag) => { merged.set(`${tag.key}:${tag.value || ''}:${tag.param || ''}`, tag); }); return Array.from(merged.values()); } extractChoicePreviewTags(choice) { const pathString = String(choice?.pathStringOnChoice || choice?.targetPath?.toString?.() || '').trim(); if (!pathString || !this.storyJson) return []; const container = this.resolveInkPath(pathString); if (!Array.isArray(container)) return []; const tags = []; for (let index = 0; index < container.length; index += 1) { const token = container[index]; if (typeof token === 'string' && token.replace(/^\^/, '').trim() === '') continue; if (token === '\n') continue; if (token !== '#') break; const rawParts = []; index += 1; while (index < container.length && container[index] !== '/#') { const part = container[index]; if (typeof part === 'string') { rawParts.push(part.replace(/^\^/, '')); } index += 1; } const tag = (0, tag_parser_1.parseTags)([rawParts.join('').trim()])[0]; if (tag && this.choicePreviewTagKeys.has(tag.key)) { tags.push(tag); } } return tags; } resolveInkPath(pathString) { const parts = pathString.split('.').filter(Boolean); let node = this.storyJson?.root; for (const part of parts) { if (!node) return null; if (Array.isArray(node) && /^\d+$/.test(part)) { node = node[Number(part)]; } else if (Array.isArray(node)) { node = this.findNamedInkChild(node, part); } else if (this.isNamedContainerMap(node) && part in node) { node = node[part]; } else { return null; } } return node; } findNamedInkChild(container, part) { for (let index = container.length - 1; index >= 0; index -= 1) { const item = container[index]; if (this.isNamedContainerMap(item) && part in item) { return item[part]; } if (!Array.isArray(item)) continue; const namedMap = this.getInkContainerMap(item); if (namedMap?.['#n'] === part) { return item; } if (namedMap && part in namedMap) { return namedMap[part]; } } return null; } getInkContainerMap(container) { for (let index = container.length - 1; index >= 0; index -= 1) { const item = container[index]; if (this.isNamedContainerMap(item)) { return item; } } return null; } isNamedContainerMap(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } } exports.InkEngine = InkEngine; //# sourceMappingURL=ink-engine.js.map