feat: make demo story screen interactive
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user