Document markup and improve choice tags

This commit is contained in:
2026-05-17 15:52:41 +02:00
parent c2fb27b6b8
commit 2c54498ee2
52 changed files with 3485 additions and 377 deletions
+3
View File
@@ -9,6 +9,7 @@ export interface GameMetadata {
subtitle?: string;
version?: string;
copyright?: string;
language?: string;
}
export interface GamePaths {
@@ -52,6 +53,7 @@ function fallbackConfig(engine: EngineName): GameEngineConfig {
subtitle: 'An open-world text adventure',
version: '1.0.0',
copyright: '',
language: 'en_US',
},
};
}
@@ -81,6 +83,7 @@ export function loadGameConfig(configPath: string, engine: EngineName): GameEngi
metadata: {
...fallback.metadata,
...(parsed.metadata ?? {}),
language: parsed.metadata?.language ?? parsed.locale ?? fallback.metadata.language,
},
};
}
+37 -2
View File
@@ -150,11 +150,13 @@ export class InkEngine {
const paragraphs: TurnResult['paragraphs'] = [];
const globalTags: StoryTag[] = [];
const turnTags: StoryTag[] = [];
while (this.story.canContinue) {
const rawText = this.story.Continue();
const text = String(rawText || '').trim();
const tags = parseTags(this.story.currentTags || []);
turnTags.push(...tags);
tags
.filter((tag) => tag.key === 'title' || tag.key === 'author')
@@ -170,7 +172,7 @@ export class InkEngine {
const choices = this.story.currentChoices.map((choice): ChoiceResult => {
const tags = parseTags(choice.tags || []);
const category = getTagValue(tags, 'action');
const letter = getTagValue(tags, 'letter');
const letter = getTagValue(tags, 'letter') || getTagValue(tags, 'key');
return {
index: choice.index,
text: String(choice.text || '').trim(),
@@ -179,13 +181,46 @@ export class InkEngine {
letter,
};
});
const inputMode = choices.length > 0 ? 'choice' : 'end';
const gameState: TurnResult['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: StoryTag = { 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: choices.length > 0 ? 'choice' : 'end',
inputMode,
globalTags: globalTags.length > 0 ? globalTags : undefined,
gameState: Object.keys(gameState).length > 0 ? gameState : undefined,
};
}
}
+4
View File
@@ -35,6 +35,10 @@ export interface TurnResult {
score?: number;
moves?: number;
statusLine?: string;
endState?: {
type: 'intended' | 'error';
message?: string;
};
};
suggestions?: string[];
}
+8
View File
@@ -23,6 +23,14 @@ export function parseTag(raw: string): StoryTag | null {
return tag;
}
const colonMatch = text.match(/^([A-Za-z][\w-]*)\s*:\s*(.*?)\s*(?:\(([^)]*)\))?$/);
if (colonMatch) {
const tag: StoryTag = { key: normalizeKey(colonMatch[1]) };
tag.value = colonMatch[2].trim();
if (typeof colonMatch[3] !== 'undefined') tag.param = colonMatch[3].trim();
return tag;
}
const bareMatch = text.match(/^[A-Za-z][\w-]*$/);
if (bareMatch) {
return { key: normalizeKey(text) };