Checkpoint current UI and ink integration state

This commit is contained in:
2026-05-18 02:46:02 +02:00
parent 2c54498ee2
commit d7bb175167
384 changed files with 922883 additions and 764 deletions
+73 -3
View File
@@ -77,6 +77,8 @@ export function compileInkSource(sourcePath: string, outputPath: string): InkCom
export class InkEngine {
private story: Story | null = null;
private nextTurnId = 1;
private storyJson: any = null;
private readonly choicePreviewTagKeys = new Set(['action', 'key', 'letter', 'optional', 'gated', 'sort']);
constructor(private readonly storyPath: string) {}
@@ -139,8 +141,8 @@ export class InkEngine {
throw new Error(`Ink story file not found: ${resolvedPath}`);
}
const storyJson = JSON.parse(readFileSync(resolvedPath, 'utf8'));
return new Story(storyJson);
this.storyJson = JSON.parse(readFileSync(resolvedPath, 'utf8'));
return new Story(this.storyJson);
}
private continueStory(): TurnResult {
@@ -170,7 +172,7 @@ export class InkEngine {
}
const choices = this.story.currentChoices.map((choice): ChoiceResult => {
const tags = parseTags(choice.tags || []);
const tags = this.getChoiceTags(choice);
const category = getTagValue(tags, 'action');
const letter = getTagValue(tags, 'letter') || getTagValue(tags, 'key');
return {
@@ -223,4 +225,72 @@ export class InkEngine {
gameState: Object.keys(gameState).length > 0 ? gameState : undefined,
};
}
private getChoiceTags(choice: any): StoryTag[] {
const directTags = parseTags(choice?.tags || []);
const previewTags = this.extractChoicePreviewTags(choice);
const merged = new Map<string, StoryTag>();
[...previewTags, ...directTags].forEach((tag) => {
merged.set(`${tag.key}:${tag.value || ''}:${tag.param || ''}`, tag);
});
return Array.from(merged.values());
}
private extractChoicePreviewTags(choice: any): StoryTag[] {
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: StoryTag[] = [];
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: string[] = [];
index += 1;
while (index < container.length && container[index] !== '/#') {
const part = container[index];
if (typeof part === 'string') {
rawParts.push(part.replace(/^\^/, ''));
}
index += 1;
}
const tag = parseTags([rawParts.join('').trim()])[0];
if (tag && this.choicePreviewTagKeys.has(tag.key)) {
tags.push(tag);
}
}
return tags;
}
private resolveInkPath(pathString: string): any {
const parts = pathString.split('.').filter(Boolean);
let node: any = this.storyJson?.root;
for (const part of parts) {
if (!node) return null;
if (Array.isArray(node) && node.length > 0 && this.isNamedContainerMap(node[node.length - 1]) && part in node[node.length - 1]) {
node = node[node.length - 1][part];
} else if (Array.isArray(node) && /^\d+$/.test(part)) {
node = node[Number(part)];
} else if (this.isNamedContainerMap(node) && part in node) {
node = node[part];
} else {
return null;
}
}
return node;
}
private isNamedContainerMap(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
}