feat: make demo story screen interactive

This commit is contained in:
Codex
2026-07-28 13:06:49 +08:00
parent ea209c9b69
commit 77b42a7879
6 changed files with 877 additions and 46 deletions
+93
View File
@@ -0,0 +1,93 @@
import type { PlayerView, PresentationBeat, TurnRequest, TurnResult } from "@contracts";
const DEMO_LATENCY_MS = 120;
function nextNodeId(currentNodeId: string): string {
const match = currentNodeId.match(/^(.*?)(\d+)$/);
if (!match) return `${currentNodeId}_next`;
const [, prefix, digits] = match;
return `${prefix}${String(Number(digits) + 1).padStart(digits.length, "0")}`;
}
function replyBeats(request: TurnRequest): PresentationBeat[] {
if (request.intent === "continue") {
return [
{
id: `${request.actionId}_rain`,
kind: "narration",
speaker: null,
text: "一阵风越过站台,雨点敲在铁棚上,短暂盖过了远处的水声。",
visual: null
},
{
id: `${request.actionId}_nana`,
kind: "dialogue",
speaker: "娜娜",
text: "如果你还没想好,就先听一会儿雨吧。",
visual: {
character: "nana",
expression: "guarded",
pose: null,
scene: null
}
}
];
}
return [
{
id: `${request.actionId}_player`,
kind: "action",
speaker: "你",
// The demo repeats only the player's literal input. It never invents player speech or intent.
text: request.input,
visual: null
},
{
id: `${request.actionId}_nana`,
kind: "dialogue",
speaker: "娜娜",
text: "娜娜静静听完,攥着外套的手稍微松开了一些。“我知道了。”",
visual: {
character: "nana",
expression: "uneasy",
pose: "holding_coat",
scene: null
}
}
];
}
/**
* Browser-only M0 adapter. It deliberately mirrors the future runtime boundary:
* App -> TurnRequest -> TurnResult. No Tauri command or persisted branch is implied.
*/
export async function submitDemoTurn(
request: TurnRequest,
currentView: PlayerView
): Promise<TurnResult> {
await new Promise((resolve) => window.setTimeout(resolve, DEMO_LATENCY_MS));
const committedNodeId = nextNodeId(currentView.nodeId);
const previousHistory = currentView.history.map((node) => ({ ...node, isCurrent: false }));
return {
committedNodeId,
playerView: {
...currentView,
nodeId: committedNodeId,
beats: [...currentView.beats, ...replyBeats(request)].slice(-8),
history: [
...previousHistory,
{
id: committedNodeId,
parentId: currentView.nodeId,
branchId: currentView.branchId,
label: request.intent === "continue" ? "雨声中的停顿" : "回应娜娜",
isCurrent: true
}
]
}
};
}