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 { 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 } ] } }; }