feat(web): route turns through runtime bridge

This commit is contained in:
Codex
2026-07-28 13:19:11 +08:00
parent c59781df08
commit dbc4d2981c
4 changed files with 123 additions and 4 deletions
+56
View File
@@ -0,0 +1,56 @@
{
"story_id": "story_nana_demo",
"current_node": "node_001",
"current_branch": "branch_main",
"world_flags": {},
"relationships": {
"nana->player": {
"affinity": 48,
"trust": 34,
"hope": 31,
"respect": 45,
"intimacy": 26,
"attachment": 42
}
},
"relationship_states": [],
"promises": [],
"knowledge": [
{
"id": "knowledge_station_clock",
"observer": "player",
"subject": "old_station_platform",
"fact": "站钟停在四点十七分,似乎并非自然损坏。",
"certainty": "confirmed",
"source": "玩家进入站台时亲眼观察",
"learned_at": "node_001",
"last_verified_at": "node_001"
}
],
"items": [
{
"id": "item_flashlight_001",
"spec_ref": "nana.item.old_flashlight",
"owner": "player",
"holder": "player",
"placement": "bag",
"quantity": 1,
"condition": "worn",
"state_tags": [],
"acquisition": {
"mode": "initial",
"from": null,
"at_node": "node_001"
}
}
],
"clocks": [
{
"id": "clock_dawn",
"label": "天亮逼近",
"value": 0,
"max": 6
}
],
"checks": []
}
+46
View File
@@ -0,0 +1,46 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import initialView from "../../fixtures/player-view/initial.json";
import type { PlayerView, TurnRequest, TurnResult } from "@contracts";
const invokeMock = vi.hoisted(() => vi.fn());
vi.mock("@tauri-apps/api/core", () => ({
invoke: invokeMock
}));
import { submitTurn } from "./bridge";
const request: TurnRequest = {
storyId: initialView.storyId,
branchId: initialView.branchId,
expectedNodeId: initialView.nodeId,
actionId: "action_bridge_test",
intent: "continue",
input: ""
};
describe("Tauri bridge", () => {
beforeEach(() => {
invokeMock.mockReset();
window.__TAURI_INTERNALS__ = {};
});
afterEach(() => {
delete window.__TAURI_INTERNALS__;
});
it("sends only TurnRequest to the submit_turn command", async () => {
const expected: TurnResult = {
committedNodeId: "node_002",
playerView: {
...(initialView as PlayerView),
nodeId: "node_002"
}
};
invokeMock.mockResolvedValue(expected);
await expect(submitTurn(request, initialView as PlayerView)).resolves.toEqual(expected);
expect(invokeMock).toHaveBeenCalledWith("submit_turn", { request });
});
});
+19 -1
View File
@@ -1,7 +1,15 @@
import { invoke } from "@tauri-apps/api/core";
import initialView from "../../fixtures/player-view/initial.json";
import type { AppInfo, DemoPackSummary, PlayerView } from "@contracts";
import type {
AppInfo,
DemoPackSummary,
PlayerView,
TurnRequest,
TurnResult
} from "@contracts";
import { submitDemoTurn } from "./turnAdapter";
function isTauri(): boolean {
return typeof window !== "undefined" && window.__TAURI_INTERNALS__ !== undefined;
@@ -40,3 +48,13 @@ export async function getDemoPackSummary(): Promise<DemoPackSummary> {
}
return invoke<DemoPackSummary>("get_demo_pack_summary");
}
export async function submitTurn(
request: TurnRequest,
currentView: PlayerView
): Promise<TurnResult> {
if (!isTauri()) {
return submitDemoTurn(request, currentView);
}
return invoke<TurnResult>("submit_turn", { request });
}
+2 -3
View File
@@ -2,8 +2,7 @@ import { onMounted, ref } from "vue";
import type { AppInfo, DemoPackSummary, PlayerView, TurnIntent, TurnRequest } from "@contracts";
import { getAppInfo, getDemoPackSummary, getDemoPlayerView } from "./bridge";
import { submitDemoTurn } from "./turnAdapter";
import { getAppInfo, getDemoPackSummary, getDemoPlayerView, submitTurn as submitRuntimeTurn } from "./bridge";
export function useDemo() {
const appInfo = ref<AppInfo | null>(null);
@@ -51,7 +50,7 @@ export function useDemo() {
lastSubmittedIntent.value = intent;
try {
const result = await submitDemoTurn(request, currentView);
const result = await submitRuntimeTurn(request, currentView);
playerView.value = result.playerView;
} catch (reason) {
turnError.value = reason instanceof Error ? reason.message : String(reason);