@@ -190,8 +190,8 @@ Rust DTO 只承载 HTTP contract 和跨 crate 稳定模型,不直接暴露 `mo
|
|||||||
8. `draft_json: String`,序列化草稿结果。
|
8. `draft_json: String`,序列化草稿结果。
|
||||||
9. `last_assistant_reply: String`。
|
9. `last_assistant_reply: String`。
|
||||||
10. `published_profile_id: String`,未发布为空字符串。
|
10. `published_profile_id: String`,未发布为空字符串。
|
||||||
11. `created_at: i64`。
|
11. `created_at: Timestamp`。
|
||||||
12. `updated_at: i64`。
|
12. `updated_at: Timestamp`。
|
||||||
|
|
||||||
## 5.2 `match3d_agent_message`
|
## 5.2 `match3d_agent_message`
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ Rust DTO 只承载 HTTP contract 和跨 crate 稳定模型,不直接暴露 `mo
|
|||||||
3. `role: String`,建议值:`user`、`assistant`、`system`。
|
3. `role: String`,建议值:`user`、`assistant`、`system`。
|
||||||
4. `kind: String`,建议值:`text`、`action`、`error`。
|
4. `kind: String`,建议值:`text`、`action`、`error`。
|
||||||
5. `text: String`。
|
5. `text: String`。
|
||||||
6. `created_at: i64`。
|
6. `created_at: Timestamp`。
|
||||||
|
|
||||||
## 5.3 `match3d_work_profile`
|
## 5.3 `match3d_work_profile`
|
||||||
|
|
||||||
@@ -227,8 +227,8 @@ Rust DTO 只承载 HTTP contract 和跨 crate 稳定模型,不直接暴露 `mo
|
|||||||
13. `config_json: String`。
|
13. `config_json: String`。
|
||||||
14. `publication_status: String`,建议值:`Draft`、`Published`。
|
14. `publication_status: String`,建议值:`Draft`、`Published`。
|
||||||
15. `play_count: u32`。
|
15. `play_count: u32`。
|
||||||
16. `updated_at: i64`。
|
16. `updated_at: Timestamp`。
|
||||||
17. `published_at: i64`,未发布为 `0`。
|
17. `published_at: Option<Timestamp>`,未发布为 `None`。
|
||||||
|
|
||||||
## 5.4 `match3d_runtime_run`
|
## 5.4 `match3d_runtime_run`
|
||||||
|
|
||||||
@@ -250,8 +250,8 @@ Rust DTO 只承载 HTTP contract 和跨 crate 稳定模型,不直接暴露 `mo
|
|||||||
12. `cleared_item_count: u32`。
|
12. `cleared_item_count: u32`。
|
||||||
13. `failure_reason: String`,建议值为空、`TimeUp`、`TrayFull`。
|
13. `failure_reason: String`,建议值为空、`TimeUp`、`TrayFull`。
|
||||||
14. `snapshot_json: String`,序列化 `Match3DRunSnapshot`。
|
14. `snapshot_json: String`,序列化 `Match3DRunSnapshot`。
|
||||||
15. `created_at: i64`。
|
15. `created_at: Timestamp`。
|
||||||
16. `updated_at: i64`。
|
16. `updated_at: Timestamp`。
|
||||||
|
|
||||||
## 5.5 `match3d_play_record`
|
## 5.5 `match3d_play_record`
|
||||||
|
|
||||||
@@ -687,6 +687,14 @@ F3 运行态即时反馈分支可以先用本地 mock snapshot 开发,但必
|
|||||||
2. 与 `module-match3d` 规则接线。
|
2. 与 `module-match3d` 规则接线。
|
||||||
3. `spacetime build` 或仓库现有等价脚本通过。
|
3. `spacetime build` 或仓库现有等价脚本通过。
|
||||||
|
|
||||||
|
B3 当前落地状态:
|
||||||
|
|
||||||
|
1. `server-rs/crates/spacetime-module/src/match3d/` 已承载 Match3D 的表、procedure 输入输出类型和 procedure 实现,并由 `server-rs/crates/spacetime-module/src/lib.rs` 挂载导出。
|
||||||
|
2. `migration.rs` 已纳入 `match3d_agent_session`、`match3d_agent_message`、`match3d_work_profile`、`match3d_runtime_run` 四张表,后续字段变更继续按 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` 追加兼容字段。
|
||||||
|
3. 运行态 `start_match3d_run`、`click_match3d_item`、`stop_match3d_run`、`finish_match3d_time_up` 通过适配层调用 `module-match3d` 的领域规则,SpacetimeDB 层只负责归属校验、事务写入、权威快照持久化和 procedure JSON 返回。
|
||||||
|
4. B3 对外仍返回当前首版快照字段 `snapshotVersion / clientSnapshotVersion` 对应语义;`module-match3d` 内部的 `board_version` 只在适配层中转换,避免影响并行中的 B4/F3 接入。
|
||||||
|
5. SpacetimeDB module 的有效验收命令是 `spacetime build --module-path crates/spacetime-module`;不要用普通 native `cargo test -p spacetime-module` 作为验收口径,因为该 crate 会链接 SpacetimeDB 宿主符号。
|
||||||
|
|
||||||
### F1:创作入口与 Agent UI
|
### F1:创作入口与 Agent UI
|
||||||
|
|
||||||
写入范围:
|
写入范围:
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ server-rs/crates/module-match3d
|
|||||||
|
|
||||||
该 crate 是纯领域层,不读写数据库,不访问网络,不依赖浏览器或文件系统。
|
该 crate 是纯领域层,不读写数据库,不访问网络,不依赖浏览器或文件系统。
|
||||||
|
|
||||||
|
本阶段虽然不落 SpacetimeDB 表和 procedure,但领域模型已经为后续 SpacetimeDB 接入预留 `spacetime-types` feature。后续在 `spacetime-module` 内使用这些类型时,仍必须遵守 reducer 确定性、`ctx.sender()` 鉴权和表结构迁移约束。
|
||||||
|
|
||||||
核心类型:
|
核心类型:
|
||||||
|
|
||||||
1. `Match3DCreatorConfig`
|
1. `Match3DCreatorConfig`
|
||||||
@@ -89,6 +91,8 @@ server-rs/crates/module-match3d
|
|||||||
|
|
||||||
`Flying` 只作为前端表现态,不要求后端逐帧落库。后端只确认物品是否已从 `InBoard` 进入 `InTray` 或 `Cleared`。
|
`Flying` 只作为前端表现态,不要求后端逐帧落库。后端只确认物品是否已从 `InBoard` 进入 `InTray` 或 `Cleared`。
|
||||||
|
|
||||||
|
运行态领域内部使用 `board_version` 表示权威快照版本;HTTP 与 TypeScript shared contracts 对外使用 `snapshotVersion` / `clientSnapshotVersion`,由后续 `api-server` facade 做字段映射。
|
||||||
|
|
||||||
## 5. 生成规则 Stage1 口径
|
## 5. 生成规则 Stage1 口径
|
||||||
|
|
||||||
1. `clearCount` 必须是正整数。
|
1. `clearCount` 必须是正整数。
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# 抓大鹅 Match3D F1 创作入口与 Agent UI 落地记录 2026-04-30
|
||||||
|
|
||||||
|
## 1. 阶段边界
|
||||||
|
|
||||||
|
本文件承接《MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md》的 F1 包。
|
||||||
|
|
||||||
|
F1 只处理前端创作入口、Agent 工作区和等待后端 B5 facade 前的 mock client。它不实现运行态规则,不修改 SpacetimeDB 表,不接 `api-server` 路由。
|
||||||
|
|
||||||
|
## 2. 本阶段写入范围
|
||||||
|
|
||||||
|
1. `src/components/platform-entry/`
|
||||||
|
2. `src/components/match3d-creation/`
|
||||||
|
3. `src/services/match3d-creation/`
|
||||||
|
4. `packages/shared/src/contracts/match3dAgent.ts`
|
||||||
|
|
||||||
|
其中 `packages/shared/src/contracts/match3dAgent.ts` 作为 F1 与后续 B5 的 DTO 对齐点,F1 mock client 不自建脱离共享契约的临时类型。
|
||||||
|
|
||||||
|
## 3. 入口接入
|
||||||
|
|
||||||
|
平台入口新增可见创作类型:
|
||||||
|
|
||||||
|
```text
|
||||||
|
id: match3d
|
||||||
|
title: 抓大鹅
|
||||||
|
subtitle: 经典消除玩法
|
||||||
|
badge: 可创建
|
||||||
|
```
|
||||||
|
|
||||||
|
入口来源统一走 `getVisiblePlatformCreationTypes()`,因此创作首页首屏卡带与“选择创作类型”弹层会同时出现抓大鹅。
|
||||||
|
|
||||||
|
## 4. Agent 工作区
|
||||||
|
|
||||||
|
新增 `Match3DAgentWorkspace`,复用通用 `CreationAgentWorkspace`。
|
||||||
|
|
||||||
|
Agent 只收集三类锚点:
|
||||||
|
|
||||||
|
1. 题材主题。
|
||||||
|
2. 需要消除次数。
|
||||||
|
3. 难度。
|
||||||
|
|
||||||
|
工作区支持参考图片上传入口。图片在 F1 中先以 Data URL 形式随消息 payload 带给 mock client;B5 接入后由后端 facade 替换为正式资产上传与引用。
|
||||||
|
|
||||||
|
UI 中不默认展示玩法规则长文,只展示进度、锚点、聊天内容和必要按钮。
|
||||||
|
|
||||||
|
## 5. mock client
|
||||||
|
|
||||||
|
新增 `src/services/match3d-creation/match3dCreationClient.ts`。
|
||||||
|
|
||||||
|
mock client 提供:
|
||||||
|
|
||||||
|
1. `createMatch3DCreationSession`
|
||||||
|
2. `getMatch3DCreationSession`
|
||||||
|
3. `streamMatch3DCreationMessage`
|
||||||
|
4. `executeMatch3DCreationAction`
|
||||||
|
|
||||||
|
mock 行为:
|
||||||
|
|
||||||
|
1. 创建本地会话。
|
||||||
|
2. 从中文输入中提取题材、消除次数和难度。
|
||||||
|
3. 支持“自动配置”。
|
||||||
|
4. 当三项配置完整时允许执行 `match3d_compile_draft`。
|
||||||
|
5. 编译后返回 `draft_ready` 会话和草稿。
|
||||||
|
|
||||||
|
## 6. 结果承接
|
||||||
|
|
||||||
|
F1 新增 `Match3DDraftReadyView` 作为草稿生成后的临时承接页,只展示草稿基础信息并允许返回 Agent 修改。
|
||||||
|
|
||||||
|
正式结果页的基础信息编辑、封面图、试玩、发布由 F2 接入,F1 不在这里模拟发布。
|
||||||
|
|
||||||
|
## 7. 后续替换点
|
||||||
|
|
||||||
|
B5 完成后,只需要把 `match3dCreationClient` 的本地 Map mock 替换为 HTTP/SSE facade:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /api/creation/match3d/sessions
|
||||||
|
GET /api/creation/match3d/sessions/:sessionId
|
||||||
|
POST /api/creation/match3d/sessions/:sessionId/messages/stream
|
||||||
|
POST /api/creation/match3d/sessions/:sessionId/compile
|
||||||
|
```
|
||||||
|
|
||||||
|
`PlatformEntryFlowShellImpl` 与 `Match3DAgentWorkspace` 不应再改一轮业务字段。
|
||||||
|
|
||||||
|
## 8. 验收口径
|
||||||
|
|
||||||
|
1. 创作首页能看到“抓大鹅 / 经典消除玩法”。
|
||||||
|
2. 弹层选择“抓大鹅”能进入 Agent 工作区。
|
||||||
|
3. 输入题材、消除次数、难度后进度到 `100%`。
|
||||||
|
4. 点击“生成结果页”进入草稿承接页。
|
||||||
|
5. 可从草稿承接页返回 Agent 修改。
|
||||||
|
6. `npm run check:encoding` 通过。
|
||||||
|
7. `npm run typecheck` 通过。
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
|
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
|
||||||
- [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md):冻结抓大鹅 Match3D 首版 demo 的独立玩法域、表与 procedure、HTTP facade、前端即时反馈/后端权威确认协议,以及可并行开发包。
|
- [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md):冻结抓大鹅 Match3D 首版 demo 的独立玩法域、表与 procedure、HTTP facade、前端即时反馈/后端权威确认协议,以及可并行开发包。
|
||||||
- [MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md](./MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md):冻结抓大鹅 Match3D B1+B2 的纯领域规则 crate、Rust/TypeScript shared contracts,以及 Stage1 不触碰 SpacetimeDB 表和 api-server 的边界。
|
- [MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md](./MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md):冻结抓大鹅 Match3D B1+B2 的纯领域规则 crate、Rust/TypeScript shared contracts,以及 Stage1 不触碰 SpacetimeDB 表和 api-server 的边界。
|
||||||
|
- [MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md](./MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md):记录抓大鹅 F1 创作入口、Agent 工作区、参考图入口、本地 mock client 与后续 B5 HTTP facade 替换点。
|
||||||
- [PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md](./PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md):记录平台首页底部 dock 在手机浏览器地址栏展开时脱离可见区域的根因,以及 `100dvh`、固定底部锚点和安全区占位的修复口径。
|
- [PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md](./PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md):记录平台首页底部 dock 在手机浏览器地址栏展开时脱离可见区域的根因,以及 `100dvh`、固定底部锚点和安全区占位的修复口径。
|
||||||
- [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md):记录 SpacetimeDB private 表迁移 JSON 导出/导入 procedure、迁移操作员授权、HTTP 413 分片导入、Jenkins 自动迁移回灌和导入脚本参数。
|
- [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md):记录 SpacetimeDB private 表迁移 JSON 导出/导入 procedure、迁移操作员授权、HTTP 413 分片导入、Jenkins 自动迁移回灌和导入脚本参数。
|
||||||
- [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md):记录 `Genarrative-Database-Export` / `Genarrative-Database-Import` 两条 SCM-backed 数据库迁移流水线参数、默认 dry-run、token 边界和 `CHUNK_SIZE` 413 规避参数。
|
- [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md):记录 `Genarrative-Database-Export` / `Genarrative-Database-Import` 两条 SCM-backed 数据库迁移流水线参数、默认 dry-run、token 边界和 `CHUNK_SIZE` 413 规避参数。
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 抓大鹅 Match3D 创作 Agent 共享契约。
|
||||||
|
* 字段按 HTTP facade 的 camelCase DTO 命名,后端领域层 snake_case 字段由 facade 映射。
|
||||||
|
*/
|
||||||
export type Match3DCreationStage =
|
export type Match3DCreationStage =
|
||||||
|
| 'collecting'
|
||||||
| 'collecting_config'
|
| 'collecting_config'
|
||||||
|
| 'ready_to_compile'
|
||||||
| 'draft_ready'
|
| 'draft_ready'
|
||||||
|
| 'draft_compiled'
|
||||||
| 'ready_to_publish'
|
| 'ready_to_publish'
|
||||||
| 'published'
|
| 'published'
|
||||||
| string;
|
| string;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* 抓大鹅 Match3D 运行态共享契约。
|
||||||
|
* 前端可以使用 Flying 做即时表现;后端权威快照只应返回 InBoard、InTray、Cleared。
|
||||||
|
*/
|
||||||
export type Match3DRunStatus =
|
export type Match3DRunStatus =
|
||||||
| 'running'
|
| 'running'
|
||||||
| 'won'
|
| 'won'
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* 抓大鹅 Match3D 作品读写共享契约。
|
||||||
|
* 首版作品发布必须补齐游戏名称、标签、封面、题材、消除次数和难度。
|
||||||
|
*/
|
||||||
export type Match3DWorkPublicationStatus = 'draft' | 'published' | string;
|
export type Match3DWorkPublicationStatus = 'draft' | 'published' | string;
|
||||||
|
|
||||||
export interface PutMatch3DWorkRequest {
|
export interface PutMatch3DWorkRequest {
|
||||||
|
|||||||
@@ -3,14 +3,6 @@ export * from './contracts/auth';
|
|||||||
export type * from './contracts/bigFish';
|
export type * from './contracts/bigFish';
|
||||||
export * from './contracts/common';
|
export * from './contracts/common';
|
||||||
export type * from './contracts/customWorldAgent';
|
export type * from './contracts/customWorldAgent';
|
||||||
export * from './contracts/rpgAgentActions';
|
|
||||||
export * from './contracts/rpgAgentAnchors';
|
|
||||||
export * from './contracts/rpgAgentDraft';
|
|
||||||
export * from './contracts/rpgAgentSession';
|
|
||||||
export * from './contracts/rpgCreationFixtures';
|
|
||||||
export * from './contracts/rpgCreationPreview';
|
|
||||||
export * from './contracts/rpgCreationResultView';
|
|
||||||
export * from './contracts/rpgCreationWorkSummary';
|
|
||||||
export * from './contracts/match3dAgent';
|
export * from './contracts/match3dAgent';
|
||||||
export * from './contracts/match3dRuntime';
|
export * from './contracts/match3dRuntime';
|
||||||
export * from './contracts/match3dWorks';
|
export * from './contracts/match3dWorks';
|
||||||
@@ -20,6 +12,14 @@ export * from './contracts/puzzleAgentSession';
|
|||||||
export * from './contracts/puzzleResultPreview';
|
export * from './contracts/puzzleResultPreview';
|
||||||
export * from './contracts/puzzleRuntimeSession';
|
export * from './contracts/puzzleRuntimeSession';
|
||||||
export * from './contracts/puzzleWorkSummary';
|
export * from './contracts/puzzleWorkSummary';
|
||||||
|
export * from './contracts/rpgAgentActions';
|
||||||
|
export * from './contracts/rpgAgentAnchors';
|
||||||
|
export * from './contracts/rpgAgentDraft';
|
||||||
|
export * from './contracts/rpgAgentSession';
|
||||||
|
export * from './contracts/rpgCreationFixtures';
|
||||||
|
export * from './contracts/rpgCreationPreview';
|
||||||
|
export * from './contracts/rpgCreationResultView';
|
||||||
|
export * from './contracts/rpgCreationWorkSummary';
|
||||||
export * from './contracts/rpgRuntimeChat';
|
export * from './contracts/rpgRuntimeChat';
|
||||||
export * from './contracts/rpgRuntimeQuestAssist';
|
export * from './contracts/rpgRuntimeQuestAssist';
|
||||||
export * from './contracts/rpgRuntimeStoryAction';
|
export * from './contracts/rpgRuntimeStoryAction';
|
||||||
|
|||||||
1
server-rs/Cargo.lock
generated
1
server-rs/Cargo.lock
generated
@@ -2691,6 +2691,7 @@ dependencies = [
|
|||||||
"module-combat",
|
"module-combat",
|
||||||
"module-custom-world",
|
"module-custom-world",
|
||||||
"module-inventory",
|
"module-inventory",
|
||||||
|
"module-match3d",
|
||||||
"module-npc",
|
"module-npc",
|
||||||
"module-progression",
|
"module-progression",
|
||||||
"module-puzzle",
|
"module-puzzle",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub const MATCH3D_MAX_DIFFICULTY: u32 = 10;
|
|||||||
pub const MATCH3D_DEFAULT_DURATION_LIMIT_MS: u64 = 10 * 60 * 1000;
|
pub const MATCH3D_DEFAULT_DURATION_LIMIT_MS: u64 = 10 * 60 * 1000;
|
||||||
pub const MATCH3D_BOARD_RADIUS: f32 = 1.0;
|
pub const MATCH3D_BOARD_RADIUS: f32 = 1.0;
|
||||||
|
|
||||||
|
// 首版 demo 使用固定 10 组颜色形状 key;后续真实题材素材接入时仍保持 item_type_id 三个一组。
|
||||||
const MATCH3D_DEMO_VISUAL_KEYS: [&str; 10] = [
|
const MATCH3D_DEMO_VISUAL_KEYS: [&str; 10] = [
|
||||||
"red_circle",
|
"red_circle",
|
||||||
"yellow_triangle",
|
"yellow_triangle",
|
||||||
@@ -163,6 +164,7 @@ pub struct Match3DRunSnapshot {
|
|||||||
pub clear_count: u32,
|
pub clear_count: u32,
|
||||||
pub total_item_count: u32,
|
pub total_item_count: u32,
|
||||||
pub cleared_item_count: u32,
|
pub cleared_item_count: u32,
|
||||||
|
/// 领域内部权威快照版本;HTTP DTO 对外映射为 snapshotVersion。
|
||||||
pub board_version: u64,
|
pub board_version: u64,
|
||||||
pub items: Vec<Match3DItemSnapshot>,
|
pub items: Vec<Match3DItemSnapshot>,
|
||||||
pub tray_slots: Vec<Match3DTraySlot>,
|
pub tray_slots: Vec<Match3DTraySlot>,
|
||||||
@@ -303,6 +305,7 @@ pub fn build_creator_config(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 根据已确认的题材、消除次数和难度编译首版结果草稿。
|
||||||
pub fn compile_result_draft(config: &Match3DCreatorConfig) -> Match3DResultDraft {
|
pub fn compile_result_draft(config: &Match3DCreatorConfig) -> Match3DResultDraft {
|
||||||
let game_name = format!("{}抓大鹅", config.theme_text);
|
let game_name = format!("{}抓大鹅", config.theme_text);
|
||||||
let summary = format!(
|
let summary = format!(
|
||||||
@@ -310,9 +313,7 @@ pub fn compile_result_draft(config: &Match3DCreatorConfig) -> Match3DResultDraft
|
|||||||
config.theme_text, config.clear_count, config.difficulty
|
config.theme_text, config.clear_count, config.difficulty
|
||||||
);
|
);
|
||||||
let tags = default_tags_for_theme(&config.theme_text);
|
let tags = default_tags_for_theme(&config.theme_text);
|
||||||
let blockers = validate_basic_publish_fields(&game_name, &summary, &tags);
|
let mut draft = Match3DResultDraft {
|
||||||
|
|
||||||
Match3DResultDraft {
|
|
||||||
game_name,
|
game_name,
|
||||||
theme_text: config.theme_text.clone(),
|
theme_text: config.theme_text.clone(),
|
||||||
summary,
|
summary,
|
||||||
@@ -321,13 +322,18 @@ pub fn compile_result_draft(config: &Match3DCreatorConfig) -> Match3DResultDraft
|
|||||||
reference_image_src: config.reference_image_src.clone(),
|
reference_image_src: config.reference_image_src.clone(),
|
||||||
clear_count: config.clear_count,
|
clear_count: config.clear_count,
|
||||||
difficulty: config.difficulty,
|
difficulty: config.difficulty,
|
||||||
publish_ready: blockers.is_empty(),
|
publish_ready: false,
|
||||||
blockers,
|
blockers: Vec::new(),
|
||||||
}
|
};
|
||||||
|
draft.blockers = validate_result_publish_fields(&draft);
|
||||||
|
draft.publish_ready = draft.blockers.is_empty();
|
||||||
|
|
||||||
|
draft
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 校验发布所需基础字段;试玩通关不是首版发布门槛。
|
||||||
pub fn validate_publish_requirements(draft: &Match3DResultDraft) -> Vec<String> {
|
pub fn validate_publish_requirements(draft: &Match3DResultDraft) -> Vec<String> {
|
||||||
let mut blockers = validate_basic_publish_fields(&draft.game_name, &draft.summary, &draft.tags);
|
let mut blockers = validate_result_publish_fields(draft);
|
||||||
if draft.clear_count == 0 {
|
if draft.clear_count == 0 {
|
||||||
blockers.push("需要消除次数必须为正整数".to_string());
|
blockers.push("需要消除次数必须为正整数".to_string());
|
||||||
}
|
}
|
||||||
@@ -337,6 +343,7 @@ pub fn validate_publish_requirements(draft: &Match3DResultDraft) -> Vec<String>
|
|||||||
blockers
|
blockers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 将结果草稿转换为可保存的作品 profile,实际持久化由 SpacetimeDB 分支负责。
|
||||||
pub fn create_work_profile(
|
pub fn create_work_profile(
|
||||||
work_id: String,
|
work_id: String,
|
||||||
profile_id: String,
|
profile_id: String,
|
||||||
@@ -371,6 +378,7 @@ pub fn create_work_profile(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 发布作品时只改变发布状态和时间戳,不在领域层写数据库。
|
||||||
pub fn publish_work_profile(
|
pub fn publish_work_profile(
|
||||||
profile: &Match3DWorkProfile,
|
profile: &Match3DWorkProfile,
|
||||||
published_at_micros: i64,
|
published_at_micros: i64,
|
||||||
@@ -389,6 +397,7 @@ pub fn publish_work_profile(
|
|||||||
Ok(next)
|
Ok(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 用确定性 seed 生成单局初始快照,便于后端权威复现和测试。
|
||||||
pub fn start_run_with_seed_at(
|
pub fn start_run_with_seed_at(
|
||||||
run_id: String,
|
run_id: String,
|
||||||
owner_user_id: String,
|
owner_user_id: String,
|
||||||
@@ -428,6 +437,7 @@ pub fn start_run_with_seed_at(
|
|||||||
Ok(run)
|
Ok(run)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 后端权威确认一次点击:校验版本、可点击性、入槽、三消和胜负。
|
||||||
pub fn confirm_click_at(
|
pub fn confirm_click_at(
|
||||||
run: &Match3DRunSnapshot,
|
run: &Match3DRunSnapshot,
|
||||||
input: &Match3DClickInput,
|
input: &Match3DClickInput,
|
||||||
@@ -502,6 +512,7 @@ pub fn confirm_click_at(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 根据权威时间刷新剩余时间;前端本地倒计时归零后仍需走后端确认。
|
||||||
pub fn resolve_run_timer_at(run: &Match3DRunSnapshot, now_ms: u64) -> Match3DRunSnapshot {
|
pub fn resolve_run_timer_at(run: &Match3DRunSnapshot, now_ms: u64) -> Match3DRunSnapshot {
|
||||||
let mut next = run.clone();
|
let mut next = run.clone();
|
||||||
if next.status != Match3DRunStatus::Running {
|
if next.status != Match3DRunStatus::Running {
|
||||||
@@ -517,6 +528,7 @@ pub fn resolve_run_timer_at(run: &Match3DRunSnapshot, now_ms: u64) -> Match3DRun
|
|||||||
next
|
next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 停止当前运行态,用于试玩或玩家主动退出。
|
||||||
pub fn stop_run_at(run: &Match3DRunSnapshot, stopped_action_id: String) -> Match3DRunSnapshot {
|
pub fn stop_run_at(run: &Match3DRunSnapshot, stopped_action_id: String) -> Match3DRunSnapshot {
|
||||||
let mut next = run.clone();
|
let mut next = run.clone();
|
||||||
if next.status == Match3DRunStatus::Running {
|
if next.status == Match3DRunStatus::Running {
|
||||||
@@ -527,6 +539,7 @@ pub fn stop_run_at(run: &Match3DRunSnapshot, stopped_action_id: String) -> Match
|
|||||||
next
|
next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 以 2D 圆形近似判断遮挡:完全被更高层物品覆盖的物品不可点击。
|
||||||
pub fn refresh_clickable_flags(run: &mut Match3DRunSnapshot) {
|
pub fn refresh_clickable_flags(run: &mut Match3DRunSnapshot) {
|
||||||
let board_items = run
|
let board_items = run
|
||||||
.items
|
.items
|
||||||
@@ -761,6 +774,19 @@ fn validate_basic_publish_fields(game_name: &str, summary: &str, tags: &[String]
|
|||||||
blockers
|
blockers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_result_publish_fields(draft: &Match3DResultDraft) -> Vec<String> {
|
||||||
|
let mut blockers = validate_basic_publish_fields(&draft.game_name, &draft.summary, &draft.tags);
|
||||||
|
if draft
|
||||||
|
.cover_image_src
|
||||||
|
.as_deref()
|
||||||
|
.and_then(normalize_required_string)
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
blockers.push("封面图不能为空".to_string());
|
||||||
|
}
|
||||||
|
blockers
|
||||||
|
}
|
||||||
|
|
||||||
fn default_tags_for_theme(theme_text: &str) -> Vec<String> {
|
fn default_tags_for_theme(theme_text: &str) -> Vec<String> {
|
||||||
let mut tags = vec![
|
let mut tags = vec![
|
||||||
"抓大鹅".to_string(),
|
"抓大鹅".to_string(),
|
||||||
@@ -831,6 +857,17 @@ mod tests {
|
|||||||
assert_eq!(error, Match3DFieldError::InvalidClearCount);
|
assert_eq!(error, Match3DFieldError::InvalidClearCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draft_requires_cover_before_publish() {
|
||||||
|
let mut draft = compile_result_draft(&test_config(2));
|
||||||
|
|
||||||
|
assert!(!draft.publish_ready);
|
||||||
|
assert!(draft.blockers.contains(&"封面图不能为空".to_string()));
|
||||||
|
|
||||||
|
draft.cover_image_src = Some("https://example.com/cover.png".to_string());
|
||||||
|
assert!(validate_publish_requirements(&draft).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initial_run_generates_triples() {
|
fn initial_run_generates_triples() {
|
||||||
let run = start_run_with_seed_at(
|
let run = start_run_with_seed_at(
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ pub struct StartMatch3DRunRequest {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ClickMatch3DItemRequest {
|
pub struct ClickMatch3DItemRequest {
|
||||||
|
#[serde(default)]
|
||||||
|
pub run_id: Option<String>,
|
||||||
pub item_instance_id: String,
|
pub item_instance_id: String,
|
||||||
pub client_action_id: String,
|
pub client_snapshot_version: u64,
|
||||||
pub snapshot_version: u64,
|
pub client_event_id: String,
|
||||||
|
pub clicked_at_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@@ -55,13 +58,16 @@ pub struct Match3DRunSnapshotResponse {
|
|||||||
pub profile_id: String,
|
pub profile_id: String,
|
||||||
pub owner_user_id: String,
|
pub owner_user_id: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
/// 对外 HTTP 快照版本。领域层内部字段名为 board_version,facade 需要在这里完成映射。
|
||||||
|
pub snapshot_version: u64,
|
||||||
pub started_at_ms: u64,
|
pub started_at_ms: u64,
|
||||||
pub duration_limit_ms: u64,
|
pub duration_limit_ms: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub server_now_ms: Option<u64>,
|
||||||
pub remaining_ms: u64,
|
pub remaining_ms: u64,
|
||||||
pub clear_count: u32,
|
pub clear_count: u32,
|
||||||
pub total_item_count: u32,
|
pub total_item_count: u32,
|
||||||
pub cleared_item_count: u32,
|
pub cleared_item_count: u32,
|
||||||
pub board_version: u64,
|
|
||||||
pub items: Vec<Match3DItemSnapshotResponse>,
|
pub items: Vec<Match3DItemSnapshotResponse>,
|
||||||
pub tray_slots: Vec<Match3DTraySlotResponse>,
|
pub tray_slots: Vec<Match3DTraySlotResponse>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -102,14 +108,18 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn click_match3d_item_request_uses_camel_case() {
|
fn click_match3d_item_request_uses_camel_case() {
|
||||||
let payload = serde_json::to_value(ClickMatch3DItemRequest {
|
let payload = serde_json::to_value(ClickMatch3DItemRequest {
|
||||||
|
run_id: Some("run-1".to_string()),
|
||||||
item_instance_id: "item-1".to_string(),
|
item_instance_id: "item-1".to_string(),
|
||||||
client_action_id: "action-1".to_string(),
|
client_snapshot_version: 7,
|
||||||
snapshot_version: 7,
|
client_event_id: "event-1".to_string(),
|
||||||
|
clicked_at_ms: 12_345,
|
||||||
})
|
})
|
||||||
.expect("payload should serialize");
|
.expect("payload should serialize");
|
||||||
|
|
||||||
|
assert_eq!(payload["runId"], json!("run-1"));
|
||||||
assert_eq!(payload["itemInstanceId"], json!("item-1"));
|
assert_eq!(payload["itemInstanceId"], json!("item-1"));
|
||||||
assert_eq!(payload["clientActionId"], json!("action-1"));
|
assert_eq!(payload["clientSnapshotVersion"], json!(7));
|
||||||
assert_eq!(payload["snapshotVersion"], json!(7));
|
assert_eq!(payload["clientEventId"], json!("event-1"));
|
||||||
|
assert_eq!(payload["clickedAtMs"], json!(12_345));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -775,9 +775,7 @@ fn click_match3d_item_tx(
|
|||||||
Ok(click_result(
|
Ok(click_result(
|
||||||
status,
|
status,
|
||||||
next,
|
next,
|
||||||
confirmation
|
confirmation.accepted.then_some(input.item_instance_id),
|
||||||
.accepted
|
|
||||||
.then_some(input.item_instance_id),
|
|
||||||
confirmation.cleared_item_instance_ids,
|
confirmation.cleared_item_instance_ids,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -790,6 +788,7 @@ fn stop_match3d_run_tx(
|
|||||||
let stopped_at_ms = input.stopped_at_ms.max(current_server_ms(ctx));
|
let stopped_at_ms = input.stopped_at_ms.max(current_server_ms(ctx));
|
||||||
let snapshot = deserialize_snapshot(&row.snapshot_json)?;
|
let snapshot = deserialize_snapshot(&row.snapshot_json)?;
|
||||||
let domain_run = domain_snapshot_from_snapshot(&snapshot, &row.owner_user_id);
|
let domain_run = domain_snapshot_from_snapshot(&snapshot, &row.owner_user_id);
|
||||||
|
let domain_run = resolve_domain_run_timer_at(&domain_run, to_u64_ms(stopped_at_ms));
|
||||||
let domain_run = stop_domain_run_at(&domain_run, "match3d-stop".to_string());
|
let domain_run = stop_domain_run_at(&domain_run, "match3d-stop".to_string());
|
||||||
let next = snapshot_from_domain(&domain_run, stopped_at_ms);
|
let next = snapshot_from_domain(&domain_run, stopped_at_ms);
|
||||||
persist_snapshot(ctx, &row, &next, stopped_at_ms);
|
persist_snapshot(ctx, &row, &next, stopped_at_ms);
|
||||||
@@ -966,24 +965,22 @@ fn build_initial_run_snapshot(
|
|||||||
seed,
|
seed,
|
||||||
domain_started_at_ms,
|
domain_started_at_ms,
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| DomainMatch3DRunSnapshot {
|
||||||
DomainMatch3DRunSnapshot {
|
run_id: run_id.to_string(),
|
||||||
run_id: run_id.to_string(),
|
profile_id: work.profile_id.clone(),
|
||||||
profile_id: work.profile_id.clone(),
|
owner_user_id: work.owner_user_id.clone(),
|
||||||
owner_user_id: work.owner_user_id.clone(),
|
status: DomainMatch3DRunStatus::Running,
|
||||||
status: DomainMatch3DRunStatus::Running,
|
started_at_ms: domain_started_at_ms,
|
||||||
started_at_ms: domain_started_at_ms,
|
duration_limit_ms: MATCH3D_DEFAULT_DURATION_LIMIT_MS as u64,
|
||||||
duration_limit_ms: MATCH3D_DEFAULT_DURATION_LIMIT_MS as u64,
|
remaining_ms: MATCH3D_DEFAULT_DURATION_LIMIT_MS as u64,
|
||||||
remaining_ms: MATCH3D_DEFAULT_DURATION_LIMIT_MS as u64,
|
clear_count: work.clear_count.max(1),
|
||||||
clear_count: work.clear_count.max(1),
|
total_item_count: work.clear_count.max(1).saturating_mul(3),
|
||||||
total_item_count: work.clear_count.max(1).saturating_mul(3),
|
cleared_item_count: 0,
|
||||||
cleared_item_count: 0,
|
board_version: 1,
|
||||||
board_version: 1,
|
items: Vec::new(),
|
||||||
items: Vec::new(),
|
tray_slots: Vec::new(),
|
||||||
tray_slots: Vec::new(),
|
failure_reason: None,
|
||||||
failure_reason: None,
|
last_confirmed_action_id: None,
|
||||||
last_confirmed_action_id: None,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
snapshot_from_domain(&domain_run, started_at_ms)
|
snapshot_from_domain(&domain_run, started_at_ms)
|
||||||
}
|
}
|
||||||
@@ -1256,10 +1253,7 @@ fn domain_config_from_snapshot(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_from_domain(
|
fn snapshot_from_domain(run: &DomainMatch3DRunSnapshot, server_now_ms: i64) -> Match3DRunSnapshot {
|
||||||
run: &DomainMatch3DRunSnapshot,
|
|
||||||
server_now_ms: i64,
|
|
||||||
) -> Match3DRunSnapshot {
|
|
||||||
Match3DRunSnapshot {
|
Match3DRunSnapshot {
|
||||||
run_id: run.run_id.clone(),
|
run_id: run.run_id.clone(),
|
||||||
profile_id: run.profile_id.clone(),
|
profile_id: run.profile_id.clone(),
|
||||||
@@ -1278,7 +1272,10 @@ fn snapshot_from_domain(
|
|||||||
.map(snapshot_tray_slot_from_domain)
|
.map(snapshot_tray_slot_from_domain)
|
||||||
.collect(),
|
.collect(),
|
||||||
items: run.items.iter().map(snapshot_item_from_domain).collect(),
|
items: run.items.iter().map(snapshot_item_from_domain).collect(),
|
||||||
failure_reason: run.failure_reason.map(domain_failure_to_text).map(str::to_string),
|
failure_reason: run
|
||||||
|
.failure_reason
|
||||||
|
.map(domain_failure_to_text)
|
||||||
|
.map(str::to_string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1298,7 +1295,11 @@ fn domain_snapshot_from_snapshot(
|
|||||||
total_item_count: snapshot.total_item_count,
|
total_item_count: snapshot.total_item_count,
|
||||||
cleared_item_count: snapshot.cleared_item_count,
|
cleared_item_count: snapshot.cleared_item_count,
|
||||||
board_version: snapshot.snapshot_version as u64,
|
board_version: snapshot.snapshot_version as u64,
|
||||||
items: snapshot.items.iter().map(domain_item_from_snapshot).collect(),
|
items: snapshot
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(domain_item_from_snapshot)
|
||||||
|
.collect(),
|
||||||
tray_slots: snapshot
|
tray_slots: snapshot
|
||||||
.tray_slots
|
.tray_slots
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1627,14 +1628,12 @@ mod tests {
|
|||||||
assert!(confirmation.accepted);
|
assert!(confirmation.accepted);
|
||||||
assert_eq!(confirmation.cleared_item_instance_ids.len(), 3);
|
assert_eq!(confirmation.cleared_item_instance_ids.len(), 3);
|
||||||
assert!(
|
assert!(
|
||||||
next
|
next.tray_slots
|
||||||
.tray_slots
|
|
||||||
.iter()
|
.iter()
|
||||||
.all(|slot| slot.item_instance_id.is_none())
|
.all(|slot| slot.item_instance_id.is_none())
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
next
|
next.items
|
||||||
.items
|
|
||||||
.iter()
|
.iter()
|
||||||
.all(|item| item.state == MATCH3D_ITEM_CLEARED)
|
.all(|item| item.state == MATCH3D_ITEM_CLEARED)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExecuteMatch3DActionRequest,
|
ExecuteMatch3DActionRequest,
|
||||||
Match3DAnchorItemResponse,
|
|
||||||
Match3DAgentSessionSnapshot,
|
Match3DAgentSessionSnapshot,
|
||||||
|
Match3DAnchorItemResponse,
|
||||||
SendMatch3DMessageRequest,
|
SendMatch3DMessageRequest,
|
||||||
} from '../../../packages/shared/src/contracts/match3dAgent';
|
} from '../../../packages/shared/src/contracts/match3dAgent';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ export function Match3DDraftReadyView({
|
|||||||
</div>
|
</div>
|
||||||
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 px-3 py-3">
|
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 px-3 py-3">
|
||||||
<div className="text-[11px] font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
|
<div className="text-[11px] font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
|
||||||
消除
|
物品
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-[var(--platform-text-strong)]">
|
<div className="mt-1 text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||||
{draft.clearCount} 次
|
{draft.totalItemCount ?? draft.clearCount * 3} 件
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 px-3 py-3">
|
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 px-3 py-3">
|
||||||
|
|||||||
@@ -1264,9 +1264,13 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
const match3dSession = match3dFlow.session;
|
const match3dSession = match3dFlow.session;
|
||||||
const match3dError = match3dFlow.error;
|
const match3dError = match3dFlow.error;
|
||||||
|
const setMatch3DSession = match3dFlow.setSession;
|
||||||
|
const setMatch3DError = match3dFlow.setError;
|
||||||
const isMatch3DBusy = match3dFlow.isBusy;
|
const isMatch3DBusy = match3dFlow.isBusy;
|
||||||
const streamingMatch3DReplyText = match3dFlow.streamingReplyText;
|
const streamingMatch3DReplyText = match3dFlow.streamingReplyText;
|
||||||
|
const setStreamingMatch3DReplyText = match3dFlow.setStreamingReplyText;
|
||||||
const isStreamingMatch3DReply = match3dFlow.isStreamingReply;
|
const isStreamingMatch3DReply = match3dFlow.isStreamingReply;
|
||||||
|
const setIsStreamingMatch3DReply = match3dFlow.setIsStreamingReply;
|
||||||
|
|
||||||
const puzzleSession = puzzleFlow.session;
|
const puzzleSession = puzzleFlow.session;
|
||||||
const puzzleError = puzzleFlow.error;
|
const puzzleError = puzzleFlow.error;
|
||||||
@@ -1292,12 +1296,18 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
}, [bigFishFlow]);
|
}, [bigFishFlow]);
|
||||||
|
|
||||||
const openMatch3DAgentWorkspace = useCallback(async () => {
|
const openMatch3DAgentWorkspace = useCallback(async () => {
|
||||||
match3dFlow.setSession(null);
|
setMatch3DSession(null);
|
||||||
match3dFlow.setError(null);
|
setMatch3DError(null);
|
||||||
match3dFlow.setStreamingReplyText('');
|
setStreamingMatch3DReplyText('');
|
||||||
match3dFlow.setIsStreamingReply(false);
|
setIsStreamingMatch3DReply(false);
|
||||||
await match3dFlow.openWorkspace();
|
await match3dFlow.openWorkspace();
|
||||||
}, [match3dFlow]);
|
}, [
|
||||||
|
match3dFlow,
|
||||||
|
setIsStreamingMatch3DReply,
|
||||||
|
setMatch3DError,
|
||||||
|
setMatch3DSession,
|
||||||
|
setStreamingMatch3DReplyText,
|
||||||
|
]);
|
||||||
|
|
||||||
const openPuzzleAgentWorkspace = useCallback(async () => {
|
const openPuzzleAgentWorkspace = useCallback(async () => {
|
||||||
setPuzzleRun(null);
|
setPuzzleRun(null);
|
||||||
@@ -1356,10 +1366,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setBigFishRuntimeReturnStage('platform');
|
setBigFishRuntimeReturnStage('platform');
|
||||||
setBigFishGenerationState(null);
|
setBigFishGenerationState(null);
|
||||||
setBigFishError(null);
|
setBigFishError(null);
|
||||||
match3dFlow.setSession(null);
|
setMatch3DSession(null);
|
||||||
match3dFlow.setError(null);
|
setMatch3DError(null);
|
||||||
match3dFlow.setStreamingReplyText('');
|
setStreamingMatch3DReplyText('');
|
||||||
match3dFlow.setIsStreamingReply(false);
|
setIsStreamingMatch3DReply(false);
|
||||||
setPuzzleOperation(null);
|
setPuzzleOperation(null);
|
||||||
setPuzzleWorks([]);
|
setPuzzleWorks([]);
|
||||||
setSelectedPuzzleDetail(null);
|
setSelectedPuzzleDetail(null);
|
||||||
@@ -1388,17 +1398,20 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
authUi?.user,
|
authUi?.user,
|
||||||
match3dFlow,
|
|
||||||
platformBootstrap.canReadProtectedData,
|
platformBootstrap.canReadProtectedData,
|
||||||
persistRpgAgentUiState,
|
persistRpgAgentUiState,
|
||||||
resetAutoSaveTrackingToIdle,
|
resetAutoSaveTrackingToIdle,
|
||||||
resetRpgSessionViewState,
|
resetRpgSessionViewState,
|
||||||
selectionStage,
|
selectionStage,
|
||||||
setBigFishError,
|
setBigFishError,
|
||||||
|
setIsStreamingMatch3DReply,
|
||||||
|
setMatch3DError,
|
||||||
|
setMatch3DSession,
|
||||||
setPuzzleError,
|
setPuzzleError,
|
||||||
setRpgCustomWorldError,
|
setRpgCustomWorldError,
|
||||||
setRpgGeneratedCustomWorldProfile,
|
setRpgGeneratedCustomWorldProfile,
|
||||||
setSelectionStage,
|
setSelectionStage,
|
||||||
|
setStreamingMatch3DReplyText,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCreationHubCreateType = useCallback(
|
const handleCreationHubCreateType = useCallback(
|
||||||
@@ -1492,6 +1505,14 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
}
|
}
|
||||||
}, [bigFishRun, bigFishSession, selectionStage, setSelectionStage]);
|
}, [bigFishRun, bigFishSession, selectionStage, setSelectionStage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectionStage === 'match3d-result' && !match3dSession?.draft) {
|
||||||
|
setSelectionStage(
|
||||||
|
match3dSession ? 'match3d-agent-workspace' : 'platform',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [match3dSession, selectionStage, setSelectionStage]);
|
||||||
|
|
||||||
const startBigFishRun = useCallback(() => {
|
const startBigFishRun = useCallback(() => {
|
||||||
if (!bigFishSession) {
|
if (!bigFishSession) {
|
||||||
return;
|
return;
|
||||||
@@ -2897,11 +2918,13 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
: (platformBootstrap.platformError ??
|
: (platformBootstrap.platformError ??
|
||||||
sessionController.agentWorkspaceRestoreError ??
|
sessionController.agentWorkspaceRestoreError ??
|
||||||
bigFishError ??
|
bigFishError ??
|
||||||
|
match3dError ??
|
||||||
puzzleError)
|
puzzleError)
|
||||||
}
|
}
|
||||||
onRetry={() => {
|
onRetry={() => {
|
||||||
platformBootstrap.setPlatformError(null);
|
platformBootstrap.setPlatformError(null);
|
||||||
setBigFishError(null);
|
setBigFishError(null);
|
||||||
|
setMatch3DError(null);
|
||||||
setPuzzleError(null);
|
setPuzzleError(null);
|
||||||
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
|
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
|
||||||
platformBootstrap.setPlatformError(
|
platformBootstrap.setPlatformError(
|
||||||
@@ -2914,11 +2937,15 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
void refreshPuzzleShelf();
|
void refreshPuzzleShelf();
|
||||||
}}
|
}}
|
||||||
createError={
|
createError={
|
||||||
sessionController.creationTypeError ?? bigFishError ?? puzzleError
|
sessionController.creationTypeError ??
|
||||||
|
bigFishError ??
|
||||||
|
match3dError ??
|
||||||
|
puzzleError
|
||||||
}
|
}
|
||||||
createBusy={
|
createBusy={
|
||||||
sessionController.isCreatingAgentSession ||
|
sessionController.isCreatingAgentSession ||
|
||||||
isBigFishBusy ||
|
isBigFishBusy ||
|
||||||
|
isMatch3DBusy ||
|
||||||
isPuzzleBusy
|
isPuzzleBusy
|
||||||
}
|
}
|
||||||
onCreateType={handleCreationHubCreateType}
|
onCreateType={handleCreationHubCreateType}
|
||||||
@@ -3078,7 +3105,12 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
<PlatformWorkDetailView
|
<PlatformWorkDetailView
|
||||||
entry={selectedPublicWorkDetail}
|
entry={selectedPublicWorkDetail}
|
||||||
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
|
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
|
||||||
isBusy={isPublicWorkDetailBusy || isPuzzleBusy || isBigFishBusy}
|
isBusy={
|
||||||
|
isPublicWorkDetailBusy ||
|
||||||
|
isPuzzleBusy ||
|
||||||
|
isBigFishBusy ||
|
||||||
|
isMatch3DBusy
|
||||||
|
}
|
||||||
error={publicWorkDetailError}
|
error={publicWorkDetailError}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
setPublicWorkDetailError(null);
|
setPublicWorkDetailError(null);
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type {
|
|||||||
CreateMatch3DSessionRequest,
|
CreateMatch3DSessionRequest,
|
||||||
ExecuteMatch3DActionRequest,
|
ExecuteMatch3DActionRequest,
|
||||||
Match3DActionResponse,
|
Match3DActionResponse,
|
||||||
Match3DAnchorItemResponse,
|
|
||||||
Match3DAgentMessageResponse,
|
Match3DAgentMessageResponse,
|
||||||
Match3DAgentSessionSnapshot,
|
Match3DAgentSessionSnapshot,
|
||||||
|
Match3DAnchorItemResponse,
|
||||||
Match3DCreatorConfig,
|
Match3DCreatorConfig,
|
||||||
Match3DSessionResponse,
|
Match3DSessionResponse,
|
||||||
SendMatch3DMessageRequest,
|
SendMatch3DMessageRequest,
|
||||||
@@ -24,7 +24,7 @@ let match3dSessionCounter = 0;
|
|||||||
const mockSessions = new Map<string, Match3DAgentSessionSnapshot>();
|
const mockSessions = new Map<string, Match3DAgentSessionSnapshot>();
|
||||||
|
|
||||||
function delay(ms = MOCK_RESPONSE_DELAY_MS) {
|
function delay(ms = MOCK_RESPONSE_DELAY_MS) {
|
||||||
return new Promise<void>((resolve) => window.setTimeout(resolve, ms));
|
return new Promise<void>((resolve) => globalThis.setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
function nowIso() {
|
function nowIso() {
|
||||||
@@ -189,7 +189,7 @@ function updateSessionConfig(
|
|||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
progressPercent,
|
progressPercent,
|
||||||
stage: progressPercent >= 100 ? 'ready_to_compile' : 'collecting',
|
stage: 'collecting_config',
|
||||||
anchorPack: buildAnchorPack(partialConfig),
|
anchorPack: buildAnchorPack(partialConfig),
|
||||||
config,
|
config,
|
||||||
updatedAt: nowIso(),
|
updatedAt: nowIso(),
|
||||||
@@ -237,7 +237,7 @@ export async function createMatch3DCreationSession(
|
|||||||
sessionId,
|
sessionId,
|
||||||
currentTurn: 0,
|
currentTurn: 0,
|
||||||
progressPercent: 0,
|
progressPercent: 0,
|
||||||
stage: 'collecting',
|
stage: 'collecting_config',
|
||||||
anchorPack: buildAnchorPack(partialConfig),
|
anchorPack: buildAnchorPack(partialConfig),
|
||||||
config: null,
|
config: null,
|
||||||
draft: null,
|
draft: null,
|
||||||
@@ -271,7 +271,7 @@ export async function streamMatch3DCreationMessage(
|
|||||||
await delay(120);
|
await delay(120);
|
||||||
const session = ensureMockSession(sessionId);
|
const session = ensureMockSession(sessionId);
|
||||||
const text = payload.text.trim();
|
const text = payload.text.trim();
|
||||||
const currentConfig = session.config ?? {
|
const currentConfig: Partial<Match3DCreatorConfig> = session.config ?? {
|
||||||
themeText: session.anchorPack.theme.value,
|
themeText: session.anchorPack.theme.value,
|
||||||
clearCount: Number(session.anchorPack.clearCount.value) || undefined,
|
clearCount: Number(session.anchorPack.clearCount.value) || undefined,
|
||||||
difficulty: Number(session.anchorPack.difficulty.value) || undefined,
|
difficulty: Number(session.anchorPack.difficulty.value) || undefined,
|
||||||
@@ -337,7 +337,7 @@ export async function executeMatch3DCreationAction(
|
|||||||
|
|
||||||
const nextSession = {
|
const nextSession = {
|
||||||
...session,
|
...session,
|
||||||
stage: 'draft_compiled',
|
stage: 'draft_ready',
|
||||||
progressPercent: 100,
|
progressPercent: 100,
|
||||||
config,
|
config,
|
||||||
draft: buildDraft(config),
|
draft: buildDraft(config),
|
||||||
|
|||||||
Reference in New Issue
Block a user