94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
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
|
|
}
|
|
]
|
|
}
|
|
};
|
|
}
|