feat: add wooden fish play template
This commit is contained in:
@@ -113,6 +113,30 @@
|
||||
- 验证方式:新增玩法 PRD 必须显式声明单图资产槽位和系列素材槽位;新增工作台测试确认没有默认聊天式 Agent 输入;skill 通过 `quick_validate.py`。
|
||||
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`.codex/skills/genarrative-play-type-integration/SKILL.md`、`.hermes/skills/genarrative-play-type-integration/SKILL.md`。
|
||||
|
||||
## 2026-05-20 敲木鱼玩法按完整平台纵切接入
|
||||
|
||||
- 背景:敲木鱼玩法需要对齐拼图 / 跳一跳的创作闭环,不能做成孤立 demo 或前端本地计数工具。
|
||||
- 决策:新增 `wooden-fish` 玩法,采用表单 / 图片输入工作台、单图敲击物资产槽位、敲击音效资产槽位和最多 8 条飘字配置;公开作品号前缀为 `WF-*`;运行态只在单次 run 内累计总敲击次数和词条计数。后端新增独立 `module-wooden-fish`、shared contracts、SpacetimeDB `wooden_fish_*` 表 / public views、`spacetime-client` facade 和 `/api/creation/wooden-fish/*`、`/api/runtime/wooden-fish/*` 路由,前端接入平台入口、生成页、结果页、运行态、公开详情和推荐试玩。
|
||||
- 影响范围:`CONTEXT.md`、`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`packages/shared/src/contracts/woodenFish.ts`、`server-rs/crates/shared-contracts/src/wooden_fish.rs`、`server-rs/crates/module-wooden-fish/`、`server-rs/crates/spacetime-module/src/wooden_fish*`、`server-rs/crates/spacetime-client/src/wooden_fish.rs`、`src/components/wooden-fish-*`。
|
||||
- 验证方式:执行敲木鱼契约 / module / facade / runtime model / platform entry 定向测试、`npm run typecheck`、`npm run check:encoding`、`npm run check:spacetime-schema`、`cargo check -p api-server --manifest-path server-rs\Cargo.toml`,本地 smoke 使用 mock 短信配置后检查 `/healthz`。
|
||||
- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 2026-05-21 敲木鱼敲击音效复用通用 Vidu 音效链路
|
||||
|
||||
- 背景:敲木鱼创作需要通过“敲击音效”描述生成真实短音频,不能继续由 `spacetime-client` 合成 `/generated-wooden-fish-assets/...` 假路径;同时拼图和抓大鹅音频生成入口仍需保持关闭。
|
||||
- 决策:通用 `/api/creation/audio/sound-effect` 提交 Vidu 音效任务;`/api/creation/audio/sound-effect/{task_id}/asset` 只对木鱼 `hit_sound` 目标开放,完成查询、下载、OSS 私有对象、`asset_object` 和 entity binding 写入。木鱼 `compile-draft` / `generate-hit-sound` 在 `api-server` 内复用同一 helper 生成并注入 `hitSoundAsset`,`spacetime-client` 缺少真实 `hitSoundAsset` 时拒绝编译。拼图和抓大鹅相关目标继续返回 `410 Gone`。
|
||||
- 影响范围:`server-rs/crates/api-server/src/vector_engine_audio_generation.rs`、`server-rs/crates/api-server/src/wooden_fish.rs`、`server-rs/crates/spacetime-client/src/wooden_fish.rs`、`shared-contracts` / `packages/shared` 的 `creationAudio` 契约、敲木鱼 PRD 与平台链路文档。
|
||||
- 验证方式:执行 `cargo test -p shared-contracts creation_audio --manifest-path server-rs\Cargo.toml`、`cargo test -p spacetime-client wooden_fish --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server disabled_creation_audio_targets_return_gone_except_wooden_fish_sound_effects --manifest-path server-rs\Cargo.toml`、`npm run typecheck`、`npm run check:encoding`,本地 smoke 检查 `/healthz`;真实生成需同时配置 VectorEngine 与 OSS AccessKey。
|
||||
- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 2026-05-21 敲木鱼默认敲击物使用内置透明 PNG
|
||||
|
||||
- 背景:默认敲木鱼图案若继续用“木鱼”关键词临时生成,image2 容易语义化重画并改变用户认可的原始造型。
|
||||
- 决策:默认模板使用内置资源 `/wooden-fish/default-hit-object.png` 写回 `bundled-default` 敲击物资产;仅当用户输入自定义关键词、上传参考图或主动重生成敲击物时,才走 image2 -> OSS -> asset object -> entity binding 链路。创作入口卡片、结果页、运行态和公开列表兜底统一使用该 PNG。
|
||||
- 影响范围:敲木鱼工作台默认提示词、api-server 木鱼默认资产编排、创作入口种子与迁移、平台公开卡片兜底、PRD 与平台链路文档。
|
||||
- 验证方式:默认 `compile-draft` 返回的 `hitObjectAsset.generationProvider` 应为 `bundled-default` 且 `imageSrc=/wooden-fish/default-hit-object.png`;自定义关键词或参考图仍走 image2;前端静态资源可通过 Vite 直接访问。
|
||||
- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||
|
||||
## 2026-05-19 系列素材 n*n 图集抽为 api-server 通用模块
|
||||
|
||||
- 背景:抓大鹅物品 sheet 已包含 prompt 组装、固定网格切图、绿幕 / 近白底透明化、切片 PNG 持久化和 prompt 追踪;继续留在 Match3D 私有模块会让跳一跳、后续地块 / 道具类玩法重复复制同一套算法和 OSS 元数据口径。
|
||||
|
||||
@@ -179,6 +179,14 @@
|
||||
- 验证:执行 `node scripts/generate-edutainment-tv-map-concepts.mjs --dry-run`,输出应只显示 `imageReferenceCount: 1`,不出现完整 base64。
|
||||
- 关联:`scripts/generate-edutainment-tv-map-concepts.mjs`、`docs/design/【前端体验】寓教于乐电视端乐园地图入口概念图-2026-05-18.md`。
|
||||
|
||||
## 生成图资产不能只拼 generated legacy path
|
||||
|
||||
- 现象:结果页或运行态拿到 `/generated-*-assets/.../image.png` 后图片不显示;前端 `ResolvedAssetImage` 会先调用 `/api/assets/read-url?legacyPublicPath=...`,但换签后的 OSS URL 仍指向不存在对象。
|
||||
- 原因:后端只写了看起来像生成图的 legacy path,没有真正调用 image2、上传 OSS、登记 `asset_object` 并绑定实体。`/api/assets/read-url` 只负责签名读取,不会凭空生成或补写对象。
|
||||
- 处理:玩法生成链路必须在 `api-server` 完成外部副作用:调用 VectorEngine `gpt-image-2-all`,用 `GeneratedImageAssetAdapter` 准备 `PutObject`,上传 OSS 私有对象,调用 `confirm_asset_object` 和 `bind_asset_object_to_entity`,再把返回的 `legacyPublicPath` 写入玩法 profile。
|
||||
- 验证:`cargo check -p api-server --manifest-path server-rs/Cargo.toml`;契约测试应断言前端 JSON 自带的 `hitObjectAsset` 会被忽略,spacetime-client 定向测试应断言缺少服务端注入的真实 `hitObjectAsset` 时不能编译;浏览器 Network 中 generated 图片应先换签,签名 URL 指向已存在对象。
|
||||
- 关联:`server-rs/crates/api-server/src/wooden_fish.rs`、`server-rs/crates/spacetime-client/src/wooden_fish.rs`、`src/components/ResolvedAssetImage.tsx`、`src/services/assetReadUrlService.ts`。
|
||||
|
||||
## 忘记密码后仍提示手机号或密码错误先查认证快照同步
|
||||
|
||||
- 现象:用户通过“忘记密码”重设密码后,接口返回成功或页面进入登录态,但再次使用新密码登录仍提示“手机号或密码错误”;重启后还可能出现 `Bearer JWT 版本已失效`,日志里的 token version 与本地快照不一致。
|
||||
@@ -558,7 +566,7 @@
|
||||
|
||||
- 现象:登录弹窗可以进入短信页签,但点击“获取验证码”后,手机没有收到短信。
|
||||
- 原因:本地 `.env.local` 里如果是 `SMS_AUTH_PROVIDER="mock"`,后端不会发真实短信,只会返回固定 mock 验证码;真实阿里云链路已经改为普通短信 `SendSms`,验证码由当前 `api-server` 进程本地生成、哈希存储和校验,旧 `SendSmsVerifyCode` / `CheckSmsVerifyCode` 托管验证码参数不再参与真实校验。若接口直接返回“手机号登录暂未启用”,说明当前运行中的 `api-server` 进程内 `sms_auth_enabled=false`:常见原因是修改 `.env.local` 后没有重启后端,或外层 shell 已经设置了非空 `SMS_AUTH_ENABLED` 导致 dotenv 不覆盖。历史上 cmd 里 `set SMS_AUTH_ENABLED="true"` 会把引号也传进进程,Rust bool 解析失败后保持默认 false。
|
||||
- 处理:真实短信联调时把 `.env.local` 的 `SMS_AUTH_ENABLED=true`、`SMS_AUTH_PROVIDER=aliyun` 显式打开,并确认 `ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com`、`ALIYUN_SMS_SIGN_NAME=北京亓盒网络科技`、`ALIYUN_SMS_TEMPLATE_CODE=SMS_506245486`、`ALIYUN_SMS_TEMPLATE_PARAM_KEY=code` 后重启 `api-server`;如果只想验证 UI 和账号链路,则保留 `mock` 并使用 `SMS_AUTH_MOCK_VERIFY_CODE`。Shell 临时覆盖时 PowerShell 用 `$env:SMS_AUTH_ENABLED="true"`,cmd 用 `set SMS_AUTH_ENABLED=true`,不要把引号作为值的一部分。`api-server` 重启会清掉未校验的本地验证码。
|
||||
- 处理:真实短信联调时把 `.env.local` 的 `SMS_AUTH_ENABLED=true`、`SMS_AUTH_PROVIDER=aliyun` 显式打开,并确认 `ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com`、`ALIYUN_SMS_SIGN_NAME=北京亓盒网络科技`、`ALIYUN_SMS_TEMPLATE_CODE=SMS_506245486`、`ALIYUN_SMS_TEMPLATE_PARAM_KEY=code` 后重启 `api-server`;如果只想验证 UI、账号链路或 `/healthz`,则保留 / 切换为 `mock` 并使用 `SMS_AUTH_MOCK_VERIFY_CODE`。注意 `npm run dev:api-server` 会先合并 `.env.local`,且 `SMS_AUTH_ENABLED`、`SMS_AUTH_PROVIDER`、`SMS_AUTH_MOCK_VERIFY_CODE` 属于允许本地文件覆盖的键;如果 `.env.local` 写着 `SMS_AUTH_PROVIDER=aliyun` 但没有 `ALIYUN_SMS_ACCESS_KEY_*`,单纯在 PowerShell 里临时设置 mock 仍可能被覆盖,health smoke 会在启动阶段报“阿里云短信 AccessKey 未配置”。不改 `.env.local` 的临时 smoke 可以直接运行 `cargo run -p api-server --manifest-path server-rs\Cargo.toml` 并在同一 shell 中显式设置 `SMS_AUTH_PROVIDER=mock`、`GENARRATIVE_API_HOST`、`GENARRATIVE_API_PORT`、`GENARRATIVE_SPACETIME_SERVER_URL`、`GENARRATIVE_SPACETIME_DATABASE`。Shell 临时覆盖时 PowerShell 用 `$env:SMS_AUTH_ENABLED="true"`,cmd 用 `set SMS_AUTH_ENABLED=true`,不要把引号作为值的一部分。`api-server` 重启会清掉未校验的本地验证码。
|
||||
- 验证:分别请求浏览器域名和 Rust API 直连的 `/api/auth/login-options`,都应返回 `["phone","password"]`;`api-server` 日志里 `provider=aliyun` 才说明真实短信链路已生效。需要直接确认平台层真实调用阿里云时,配置 `ALIYUN_SMS_ACCESS_KEY_ID`、`ALIYUN_SMS_ACCESS_KEY_SECRET` 和 `ALIYUN_SMS_REAL_TEST_PHONE_NUMBER` 后手动执行 `cargo test -p platform-auth --manifest-path server-rs/Cargo.toml aliyun_send_sms_real_provider_sends_verify_code -- --ignored --nocapture`。
|
||||
- 关联:`server-rs/crates/api-server/src/config.rs`、`scripts/dev-utils.mjs`、`docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md`、`docs/technical/PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md`。
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A
|
||||
|
||||
- RPG / 自定义世界创作与运行时。
|
||||
- 拼图玩法创作、草稿、发布、运行态和排行榜。
|
||||
- 敲木鱼玩法创作、草稿、发布、运行态、公开详情和分享码。
|
||||
- 抓大鹅 Match3D 创作、2D 多视角素材生成、发布和运行态。
|
||||
- 大鱼吃小鱼、方洞挑战、视觉小说、汪汪声浪和儿童向寓教于乐玩法。
|
||||
- 账号、短信 / 密码 / 微信登录、个人资料、任务、钱包、邀请码、充值、反馈、法律信息和后台管理。
|
||||
|
||||
+22
@@ -18,6 +18,28 @@ _Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成
|
||||
|
||||
## Language
|
||||
|
||||
### Wooden Fish
|
||||
|
||||
**敲木鱼**:
|
||||
轻量点击型互动玩法,玩家在单次运行中点击非功能区敲击中央物品,触发敲击音效、敲击动画、随机飘字和本次运行内的词条计数。
|
||||
_Avoid_: 长期功德账本、排行榜玩法、全局账户累计
|
||||
|
||||
**敲击物图案**:
|
||||
敲木鱼作品中被玩家点击敲击的单张物品图案;默认模板使用内置透明 PNG `/wooden-fish/default-hit-object.png`,用户自定义关键词或上传图时再使用 image2 生成最终资产,上传图只作为 image2 参考。
|
||||
_Avoid_: 直接把上传图作为运行态素材、系列素材图集
|
||||
|
||||
**敲击音效**:
|
||||
敲木鱼作品中每次有效敲击播放的短音频资产,可由描述生成、文件上传或麦克风录制产生,最终统一写回作品的敲击音效资产槽位。
|
||||
_Avoid_: 背景音乐、长音频轨道、运行态实时录音
|
||||
|
||||
**飘字**:
|
||||
每次有效敲击后从作品配置中等概率抽取词条,并在敲击物上方以“词条+1”短暂漂浮显示的文本;配置里只保存幸运、健康、财富、姻缘、幸福、事业、成功、功德等词条名本身。
|
||||
_Avoid_: 带权重奖励、账户属性、可结算货币
|
||||
|
||||
**单次 run 计数**:
|
||||
敲木鱼运行态只在当前 run 内累计总敲击次数和已出现飘字词条计数,run 结束后作为摘要保存,不形成账号级长期账本。
|
||||
_Avoid_: 用户永久功德值、跨作品累计值、排行榜积分
|
||||
|
||||
### Bark Battle
|
||||
|
||||
**汪汪声浪大作战**:
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
# 敲木鱼玩法模板 PRD 2026-05-20
|
||||
|
||||
## 1. 目标
|
||||
|
||||
新增一个可创作、可试玩、可发布的轻量休闲玩法模板:
|
||||
|
||||
```text
|
||||
敲木鱼
|
||||
```
|
||||
|
||||
模板按平台新增玩法 SOP 接入完整闭环:
|
||||
|
||||
```text
|
||||
创作入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 -> 公开详情/分享
|
||||
```
|
||||
|
||||
首版默认屏幕中央展示内置卡通透明敲击物图案 `/wooden-fish/default-hit-object.png`。玩家点击运行态非功能区时触发一次敲击:播放敲击音效、敲击物图案执行被敲击动画,并在敲击物上方随机飘出一条祝福词。顶部计数器只在某条祝福词首次出现时创建,之后该词每次出现都累加。计数仅属于当前单次 run,不进入账号长期账本。
|
||||
|
||||
## 2. 模板定位
|
||||
|
||||
模板 ID:
|
||||
|
||||
```text
|
||||
wooden-fish
|
||||
```
|
||||
|
||||
用户展示名:
|
||||
|
||||
```text
|
||||
敲木鱼
|
||||
```
|
||||
|
||||
公开作品号前缀:
|
||||
|
||||
```text
|
||||
WF-*
|
||||
```
|
||||
|
||||
体验关键词:
|
||||
|
||||
1. 单屏点击;
|
||||
2. 轻量解压;
|
||||
3. 飘字反馈;
|
||||
4. 单局累计;
|
||||
5. 可自定义敲击物、敲击音效和祝福词。
|
||||
|
||||
## 3. 与拼图创作流程的复用边界
|
||||
|
||||
可以复用:
|
||||
|
||||
1. 创作入口配置、入口开关和作品架;
|
||||
2. 表单/图片输入工作台;
|
||||
3. 生成过程页和生成中恢复;
|
||||
4. 结果页的返回编辑、局部重生成、试玩、发布;
|
||||
5. 公开列表、公开详情、分享码和推荐流分发;
|
||||
6. 平台资产对象、OSS 私有读取换签和音频资产持久化能力。
|
||||
|
||||
不复用:
|
||||
|
||||
1. 拼图关卡、棋盘、拼块、排行榜和关卡推进语义;
|
||||
2. 跳一跳地块图集和蓄力判定语义;
|
||||
3. 抓大鹅物品消除、五视角图集和容器语义;
|
||||
4. 任何长期功德账本、账号维度排行榜或全局累计。
|
||||
|
||||
## 4. 创作工具平台接入声明
|
||||
|
||||
- 工作台模式:表单/图片输入创作工作台
|
||||
- 创作链路:入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态
|
||||
- 单图资产槽位:
|
||||
- `slotId=hit-object`
|
||||
- `slotType=hit-object-image`
|
||||
- `slotName=敲击物图案`
|
||||
- 提示词来源:`hitObjectPrompt` 与可选 `hitObjectReferenceImageSrc`
|
||||
- 写回字段:`hitObjectAsset`
|
||||
- 是否允许历史图:允许
|
||||
- 是否允许 AI 重绘:允许;上传图只作为 image2 参考,最终运行态只消费 image2 生成图
|
||||
- 系列素材槽位:无;首版只有单图敲击物,不生成图集
|
||||
- 音频资产槽位:
|
||||
- `slotId=hit-sound`
|
||||
- `slotType=hit-sound-audio`
|
||||
- `slotName=敲击音效`
|
||||
- 来源:`hitSoundPrompt` 生成,或上传/麦克风录制音频
|
||||
- 写回字段:`hitSoundAsset`
|
||||
- 描述生成能力:复用通用创作音频接口 `/api/creation/audio/sound-effect` 的 VectorEngine Vidu 音效生成与 OSS 持久化链路
|
||||
- API 命名空间:
|
||||
- `/api/creation/wooden-fish/...`
|
||||
- `/api/runtime/wooden-fish/...`
|
||||
- 业务真相:
|
||||
- 后端裁决并持久化 session、work profile、发布状态、run 摘要和公开投影;
|
||||
- 前端只负责点击低延迟表现、音频播放、动画、飘字渲染和定期 checkpoint。
|
||||
- 创作工具模式例外:无
|
||||
- 验证命令:
|
||||
- `npm run check:encoding`
|
||||
- `npm run typecheck`
|
||||
- `cargo test -p shared-contracts wooden_fish --manifest-path server-rs/Cargo.toml`
|
||||
- `cargo test -p module-wooden-fish --manifest-path server-rs/Cargo.toml`
|
||||
- `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
|
||||
- `npm run spacetime:generate`
|
||||
- `npm run check:spacetime-schema`
|
||||
- `npm run dev:api-server` 后检查 `/healthz`
|
||||
|
||||
## 5. 创作输入
|
||||
|
||||
工作台提交结构化 payload,不提交聊天消息。
|
||||
|
||||
必填字段:
|
||||
|
||||
1. `templateId = "wooden-fish"`;
|
||||
2. `workTitle`:作品标题;
|
||||
3. `hitObjectPrompt`:用户想敲的对象关键词或描述,默认“默认敲击物图案,圆润木质质感,透明背景”;
|
||||
4. `floatingWords[]`:祝福词,最多 8 条,不填或清空时使用默认祝福词。
|
||||
|
||||
可选字段:
|
||||
|
||||
1. `workDescription`:作品简介;
|
||||
2. `themeTags[]`:最多 6 个标签;
|
||||
3. `hitObjectReferenceImageSrc`:上传或历史图引用,只能作为 image2 参考,不可直接进入运行态;
|
||||
4. `hitSoundPrompt`:生成音效描述;
|
||||
5. `hitSoundAsset`:用户上传或录音产生的音频资产。
|
||||
|
||||
默认祝福词:
|
||||
|
||||
```text
|
||||
幸运
|
||||
健康
|
||||
财富
|
||||
姻缘
|
||||
幸福
|
||||
事业
|
||||
成功
|
||||
功德
|
||||
```
|
||||
|
||||
`floatingWords[]` 保存词条名本身,不保存 `+1` 后缀;运行态每次敲击时再把飘字展示为“词条+1”。
|
||||
|
||||
## 6. 生成规则
|
||||
|
||||
### 6.1 敲击物图案
|
||||
|
||||
默认模板在用户未自定义关键词且未上传参考图时,`compile-draft` 使用内置透明 PNG `/wooden-fish/default-hit-object.png` 写回 `hitObjectAsset`,`generationProvider="bundled-default"`。这张图来自 image2 对原始参考图的卡通风格化重绘,固定为模板默认资源,避免默认关键词在每次生成时改变造型。
|
||||
|
||||
用户输入自定义关键词、上传参考图,或在结果页主动重生成敲击物时,`compile-draft` 与 `regenerate-hit-object` 必须为敲击物图案生成 image2 单图资产,并由 `api-server` 注入写回 `hitObjectAsset`。前端 action 请求不得自带 `hitObjectAsset` 短路生成。如果用户上传参考图,后端只能把该图作为 image2 参考图或编辑输入;运行态不得直接使用上传图。
|
||||
|
||||
落库链路固定为:`api-server` 调用 VectorEngine `gpt-image-2-all` -> 服务端上传 OSS 私有对象 -> `confirm_asset_object` 登记资产对象 -> `bind_asset_object_to_entity` 绑定到 `entityKind='wooden_fish_work'`、`slot='hit_object'`、`assetKind='wooden_fish_hit_object'` -> 把 `legacyPublicPath` 写入 `hitObjectAsset.imageSrc`。不得只拼 `/generated-wooden-fish-assets/...` 占位路径;前端会对 generated legacy path 走 `/api/assets/read-url` 换签,OSS 中没有真实对象时图片无法显示。
|
||||
|
||||
默认图案要求:
|
||||
|
||||
1. 中央主体使用 `/wooden-fish/default-hit-object.png`;
|
||||
2. 透明背景;
|
||||
3. 适合移动端居中展示;
|
||||
4. 不包含 UI、按钮、说明文字、水印或品牌标识;
|
||||
5. 图片主体需留出敲击动画缩放空间。
|
||||
|
||||
### 6.2 敲击音效
|
||||
|
||||
音效统一写回 `hitSoundAsset`。
|
||||
|
||||
生成或写回规则:
|
||||
|
||||
1. 若 payload 已包含上传/录音音频资产,`compile-draft` 跳过音效生成,直接持久化该资产;
|
||||
2. 若 payload 只包含 `hitSoundPrompt`,`api-server` 复用通用创作音频能力提交 VectorEngine Vidu `sound_effect` 任务,轮询生成结果,下载音频,写入 OSS 私有对象,登记 `asset_object`,并绑定到 `entityKind='wooden_fish_work'`、`slot='hit_sound'`、`assetKind='wooden_fish_hit_sound'`,最后注入 `hitSoundAsset`;
|
||||
3. 若两者都没有,后端生成默认“清脆短促木鱼敲击声”;
|
||||
4. 音效资产必须包含可播放地址、对象键、asset object id、来源和可选时长;
|
||||
5. 通用创作音频接口对 `wooden_fish` 的 `hit_sound` 目标不得返回 `410 Gone`,对应 `storagePrefix='wooden_fish_assets'`;
|
||||
6. `spacetime-client` 不得自行合成 `/generated-wooden-fish-assets/...` 音效占位路径,缺少真实 `hitSoundAsset` 时必须拒绝编译。
|
||||
|
||||
### 6.3 封面
|
||||
|
||||
首版封面使用 `hitObjectAsset.imageSrc` 作为 `coverImageSrc`。不单独新增第三次图片生成。
|
||||
|
||||
## 7. 契约草案
|
||||
|
||||
`WoodenFishDraft` 至少包含:
|
||||
|
||||
1. `templateId = "wooden-fish"`;
|
||||
2. `templateName = "敲木鱼"`;
|
||||
3. `profileId`;
|
||||
4. `workTitle`;
|
||||
5. `workDescription`;
|
||||
6. `themeTags[]`;
|
||||
7. `hitObjectPrompt`;
|
||||
8. `hitObjectReferenceImageSrc`;
|
||||
9. `hitSoundPrompt`;
|
||||
10. `floatingWords[]`;
|
||||
11. `hitObjectAsset`;
|
||||
12. `hitSoundAsset`;
|
||||
13. `coverImageSrc`;
|
||||
14. `generationStatus`。
|
||||
|
||||
`WoodenFishImageAsset` 至少包含:
|
||||
|
||||
1. `assetId`;
|
||||
2. `imageSrc`;
|
||||
3. `imageObjectKey`;
|
||||
4. `assetObjectId`;
|
||||
5. `generationProvider`;
|
||||
6. `prompt`;
|
||||
7. `width`;
|
||||
8. `height`。
|
||||
|
||||
`WoodenFishAudioAsset` 至少包含:
|
||||
|
||||
1. `assetId`;
|
||||
2. `audioSrc`;
|
||||
3. `audioObjectKey`;
|
||||
4. `assetObjectId`;
|
||||
5. `source = generated | uploaded | recorded | placeholder`;
|
||||
6. `prompt`;
|
||||
7. `durationMs`。
|
||||
|
||||
`WoodenFishRunSnapshot` 至少包含:
|
||||
|
||||
1. `runId`;
|
||||
2. `profileId`;
|
||||
3. `ownerUserId`;
|
||||
4. `status = playing | finished`;
|
||||
5. `totalTapCount`;
|
||||
6. `wordCounters[]`;
|
||||
7. `startedAtMs`;
|
||||
8. `updatedAtMs`;
|
||||
9. `finishedAtMs`。
|
||||
|
||||
## 8. API 草案
|
||||
|
||||
HTTP 路由:
|
||||
|
||||
```text
|
||||
POST /api/creation/wooden-fish/sessions
|
||||
GET /api/creation/wooden-fish/sessions/{sessionId}
|
||||
POST /api/creation/wooden-fish/sessions/{sessionId}/actions
|
||||
GET /api/creation/wooden-fish/works/{profileId}
|
||||
POST /api/creation/wooden-fish/works/{profileId}/publish
|
||||
GET /api/runtime/wooden-fish/works/{profileId}
|
||||
POST /api/runtime/wooden-fish/runs
|
||||
POST /api/runtime/wooden-fish/runs/{runId}/checkpoint
|
||||
POST /api/runtime/wooden-fish/runs/{runId}/finish
|
||||
GET /api/runtime/wooden-fish/gallery
|
||||
GET /api/runtime/wooden-fish/gallery/{publicWorkCode}
|
||||
```
|
||||
|
||||
动作类型:
|
||||
|
||||
```text
|
||||
compile-draft
|
||||
regenerate-hit-object
|
||||
generate-hit-sound
|
||||
replace-hit-sound
|
||||
update-work-meta
|
||||
update-floating-words
|
||||
publish
|
||||
start-run
|
||||
checkpoint
|
||||
finish
|
||||
```
|
||||
|
||||
`compile-draft` 是长耗时动作。前端进入生成页后应展示可恢复进度;如果请求失败,标记失败前必须复读 session,确认后端是否已经生成并写回草稿。
|
||||
|
||||
## 9. SpacetimeDB 表和 view
|
||||
|
||||
新增表:
|
||||
|
||||
1. `wooden_fish_agent_session`;
|
||||
2. `wooden_fish_work_profile`;
|
||||
3. `wooden_fish_runtime_run`;
|
||||
4. `wooden_fish_event`。
|
||||
|
||||
新增 view:
|
||||
|
||||
1. `wooden_fish_gallery_card_view`:公开列表卡片投影,只暴露已发布作品;
|
||||
2. `wooden_fish_gallery_view`:公开详情兼容投影,包含图案、音效和祝福词配置。
|
||||
|
||||
新增或调整表、procedure、view 后必须同步 `migration.rs`、后端表目录、生成 bindings,并执行 `npm run check:spacetime-schema`。
|
||||
|
||||
## 10. 结果页能力
|
||||
|
||||
结果页必须展示:
|
||||
|
||||
1. 作品标题和简介;
|
||||
2. 敲击物图案;
|
||||
3. 敲击音效试听;
|
||||
4. 祝福词配置;
|
||||
5. 标签;
|
||||
6. 试玩;
|
||||
7. 发布;
|
||||
8. 返回编辑。
|
||||
|
||||
结果页必须支持:
|
||||
|
||||
1. 重生成敲击物图案;
|
||||
2. 生成、上传或替换敲击音效;
|
||||
3. 修改标题、简介和标签;
|
||||
4. 修改祝福词,最多 8 条。
|
||||
|
||||
图案重生成和音效生成是独立局部生成态,不得把已有可查看结果重新变成不可打开的全局生成中。
|
||||
|
||||
## 11. 运行态规则
|
||||
|
||||
运行态采用全屏单击模型。
|
||||
|
||||
功能区:
|
||||
|
||||
1. 顶部计数器;
|
||||
2. 设置、暂停、返回、发布分享等按钮;
|
||||
3. 结果弹层和音频授权提示。
|
||||
|
||||
点击规则:
|
||||
|
||||
1. 点击非功能区才算一次敲击;
|
||||
2. 每次敲击立即本地累加 `totalTapCount`;
|
||||
3. 随机等概率从 `floatingWords[]` 中取一个词条;
|
||||
4. 若词条首次出现,顶部创建对应计数器;
|
||||
5. 后续同词条出现时对应计数器 +1;
|
||||
6. 播放敲击音效;
|
||||
7. 敲击物图案执行压缩、回弹或轻微震动动画;
|
||||
8. 木鱼上方飘出“词条+1”并淡出。
|
||||
|
||||
音频播放:
|
||||
|
||||
1. 前端使用小复音池;
|
||||
2. 设置最小播放间隔,避免极端连点导致浏览器抖动;
|
||||
3. 点击计数不能因为音频节流而丢失;
|
||||
4. 签名 URL 未就绪时先静音表现,不请求裸 generated 私有路径。
|
||||
|
||||
后端只保存 run 摘要,不保存每次点击的完整明细;`checkpoint` 和 `finish` 都写入总敲击次数与词条计数快照。
|
||||
|
||||
## 12. 公开链路
|
||||
|
||||
平台首页推荐、发现、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='wooden-fish'` 与 `WF-*` 公开作品号识别敲木鱼作品。
|
||||
|
||||
公开列表优先消费 `wooden_fish_gallery_card_view` 订阅缓存。公开详情如果卡片摘要不足以进入运行态,必须补读完整 work profile。
|
||||
|
||||
## 13. 验收
|
||||
|
||||
1. 创作入口能看到 `敲木鱼` 模板;
|
||||
2. 工作台可以填写敲击物描述、上传参考图、配置音效和祝福词;
|
||||
3. 提交后生成 image2 敲击物图案;
|
||||
4. 上传图不会直接进入运行态;
|
||||
5. 用户上传或录制音效时跳过音效生成并持久化该资产;
|
||||
6. 结果页能看到图案、试听音效、编辑祝福词并试玩;
|
||||
7. 运行态功能区点击不触发敲击;
|
||||
8. 非功能区点击会计数、播放音效、播放敲击动画并飘字;
|
||||
9. 顶部计数器只在词条首次出现时创建;
|
||||
10. 连点不丢计数;
|
||||
11. `checkpoint` 和 `finish` 只保存单次 run 摘要;
|
||||
12. 作品可以发布、进入公开列表和公开详情;
|
||||
13. `WF-*` 公开作品号能进入分享和运行态;
|
||||
14. `npm run check:encoding` 通过;
|
||||
15. schema 变更后 `npm run check:spacetime-schema` 通过。
|
||||
@@ -44,7 +44,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
`server-rs/crates/spacetime-client/src/mapper.rs` 只作为聚合入口,负责声明 `src/mapper/` 下的领域子模块并 re-export 原有 record / mapper 能力;不要在该文件继续堆叠大段映射实现。
|
||||
|
||||
当前子模块按调用领域拆分:`assets.rs`、`auth.rs`、`runtime.rs`、`runtime_profile.rs`、`custom_world.rs`、`puzzle.rs`、`match3d.rs`、`square_hole.rs`、`visual_novel.rs`、`big_fish.rs`、`story.rs`、`ai.rs`、`bark_battle.rs`、`combat.rs`、`inventory.rs`、`npc.rs`,跨领域轻量 helper 和共享 record 统一放在 `common.rs`。该拆分只改变 `spacetime-client` 文件组织,不改变 SpacetimeDB schema、生成绑定、procedure result 契约或外部 DTO;后续新增 mapper 时优先落到对应领域子模块,不得重新引入跨层 JSON 字符串兼容结构。
|
||||
当前子模块按调用领域拆分:`assets.rs`、`auth.rs`、`runtime.rs`、`runtime_profile.rs`、`custom_world.rs`、`puzzle.rs`、`match3d.rs`、`jump_hop.rs`、`wooden_fish.rs`、`square_hole.rs`、`visual_novel.rs`、`big_fish.rs`、`story.rs`、`ai.rs`、`bark_battle.rs`、`combat.rs`、`inventory.rs`、`npc.rs`,跨领域轻量 helper 和共享 record 统一放在 `common.rs`。该拆分只改变 `spacetime-client` 文件组织,不改变 SpacetimeDB schema、生成绑定、procedure result 契约或外部 DTO;后续新增 mapper 时优先落到对应领域子模块,不得重新引入跨层 JSON 字符串兼容结构。
|
||||
|
||||
## API 路由分组
|
||||
|
||||
@@ -60,6 +60,7 @@ npm run check:server-rs-ddd
|
||||
- 自定义世界 / RPG:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。
|
||||
- 拼图:`/api/runtime/puzzle/*`。
|
||||
- 抓大鹅 Match3D:`/api/creation/match3d/*`、`/api/runtime/match3d/*`。
|
||||
- 敲木鱼:`/api/creation/wooden-fish/*`、`/api/runtime/wooden-fish/*`。
|
||||
- 方洞挑战:`/api/creation/square-hole/*`、`/api/runtime/square-hole/*`。
|
||||
- 视觉小说:`/api/creation/visual-novel/*`、`/api/runtime/visual-novel/*`。
|
||||
- 大鱼吃小鱼:`/api/runtime/big-fish/*`。
|
||||
@@ -156,7 +157,7 @@ npm run check:server-rs-ddd
|
||||
- Match3D 封面和 9:16 纯背景:VectorEngine `/v1/images/generations`。
|
||||
- Match3D 1:1 容器 UI:VectorEngine `/v1/images/edits` multipart 参考图。
|
||||
- Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;新 Match3D 草稿和批量新增不再生成 GLB。
|
||||
- 音频:视觉小说专用音频路由保留;拼图和抓大鹅生成入口暂时关闭,通用 `/api/creation/audio/*` 对相关目标返回 `410 Gone`。
|
||||
- 音频:视觉小说专用音频路由保留;拼图和抓大鹅生成入口暂时关闭,通用 `/api/creation/audio/*` 对相关目标返回 `410 Gone`;敲木鱼 `hit_sound` 目标例外开放,复用 VectorEngine Vidu 音效生成、OSS 私有对象、`asset_object` 和 entity binding 链路,目标字段固定为 `entityKind='wooden_fish_work'`、`slot='hit_sound'`、`assetKind='wooden_fish_hit_sound'`、`storagePrefix='wooden_fish_assets'`。
|
||||
- OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。
|
||||
|
||||
## SpacetimeDB 表目录
|
||||
@@ -396,6 +397,40 @@ npm run check:server-rs-ddd
|
||||
- 源码:`server-rs/crates/spacetime-module/src/jump_hop.rs`
|
||||
- 说明:跳一跳公开详情兼容投影,包含作品、路径和素材字段;公开列表主路径优先使用 `jump_hop_gallery_card_view`。
|
||||
|
||||
### `wooden_fish_agent_session`
|
||||
|
||||
- Rust 结构体:`WoodenFishAgentSessionRow`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/wooden_fish/tables.rs`
|
||||
|
||||
### `wooden_fish_event`
|
||||
|
||||
- Rust 结构体:`WoodenFishEventRow`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/wooden_fish/tables.rs`
|
||||
|
||||
### `wooden_fish_runtime_run`
|
||||
|
||||
- Rust 结构体:`WoodenFishRuntimeRunRow`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/wooden_fish/tables.rs`
|
||||
|
||||
### `wooden_fish_work_profile`
|
||||
|
||||
- Rust 结构体:`WoodenFishWorkProfileRow`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/wooden_fish/tables.rs`
|
||||
|
||||
### SpacetimeDB view:`wooden_fish_gallery_card_view`
|
||||
|
||||
- Rust view:`wooden_fish_gallery_card_view`
|
||||
- 返回类型:`Vec<WoodenFishGalleryCardViewRow>`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/wooden_fish.rs`
|
||||
- 说明:敲木鱼公开广场列表卡片投影,只暴露 `publication_status = published` 的作品卡片字段;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM wooden_fish_gallery_card_view` 后,从本地 cache 构造敲木鱼公开列表响应。个人作品列表、详情、发布和运行态仍按 procedure 路径处理。
|
||||
|
||||
### SpacetimeDB view:`wooden_fish_gallery_view`
|
||||
|
||||
- Rust view:`wooden_fish_gallery_view`
|
||||
- 返回类型:`Vec<WoodenFishGalleryViewRow>`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/wooden_fish.rs`
|
||||
- 说明:敲木鱼公开详情兼容投影,包含敲击物图案、敲击音效和飘字配置;公开列表主路径优先使用 `wooden_fish_gallery_card_view`。
|
||||
|
||||
### `match3d_agent_message`
|
||||
|
||||
- Rust 结构体:`Match3DAgentMessageRow`
|
||||
|
||||
@@ -87,6 +87,26 @@
|
||||
|
||||
平台首页推荐、精选、最新、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='jump-hop'` 与 `JH-*` 公开作品号识别跳一跳作品;从公开详情或推荐流启动运行态时,若卡片摘要不足以携带角色图、地块图集和路径配置,必须先补读完整 work profile 再传入运行态。
|
||||
|
||||
## 敲木鱼
|
||||
|
||||
对外名称:`敲木鱼`。工程域:`wooden-fish`。PRD 见 `docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`。
|
||||
|
||||
首版定位为单屏点击解压模板,链路对齐拼图的创作闭环:
|
||||
|
||||
```text
|
||||
创作入口 -> 工作台 -> 生成过程页 -> 结果页 -> 试玩 -> 发布 -> 运行态
|
||||
```
|
||||
|
||||
创作输入固定为:
|
||||
|
||||
1. `敲什么`:单图资产槽位。默认模板使用内置透明 PNG `/wooden-fish/default-hit-object.png` 作为 `bundled-default` 敲击物资产,避免默认关键词被重新语义化改形;用户输入自定义关键词或上传参考图时,后端必须使用 image2 生成最终敲击物图案,上传图只作为参考,不直接进入运行态。自定义 `compile-draft` / `regenerate-hit-object` 必须完成 image2 -> OSS 私有对象 -> asset object 登记和绑定后,再由 `api-server` 注入真实 `hitObjectAsset.imageSrc`,不能只写 `/generated-wooden-fish-assets/...` 占位路径,也不能接受前端请求自带的 `hitObjectAsset` 短路生成。
|
||||
2. `敲击音效`:音频资产槽位,支持描述生成、上传和麦克风录制,统一写回 `hitSoundAsset`。描述生成复用通用 `/api/creation/audio/sound-effect` 的 VectorEngine Vidu 音效生成、下载、OSS 私有对象、asset object 登记和 entity binding 链路;木鱼目标固定为 `entityKind='wooden_fish_work'`、`slot='hit_sound'`、`assetKind='wooden_fish_hit_sound'`、`storagePrefix='wooden_fish_assets'`,不得再返回 `410 Gone`,也不得由 `spacetime-client` 合成假音频路径。
|
||||
3. `功德有什么`:最多 8 条飘字,默认 `幸运、健康、财富、姻缘、幸福、事业、成功、功德`;创作态只保存词条名,运行态飘字展示时再追加 `+1`。
|
||||
|
||||
运行态规则真相以后端 run 摘要为准,前端只做点击低延迟表现、敲击动画、音频播放和飘字渲染。每次非功能区点击在当前 run 内累计 `totalTapCount` 和 `wordCounters`;计数不进入账号长期账本,不做排行榜。顶部计数器仅在词条首次出现时创建,后续同词条继续累加。
|
||||
|
||||
平台首页推荐、精选、最新、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='wooden-fish'` 与 `WF-*` 公开作品号识别敲木鱼作品;公开列表应走 `wooden_fish_gallery_card_view` 订阅缓存,公开详情或运行态启动时卡片摘要不足则补读完整 work profile。
|
||||
|
||||
## 抓大鹅 Match3D
|
||||
|
||||
对外名称:`抓大鹅`。工程域:`match3d`。
|
||||
|
||||
@@ -39,7 +39,12 @@ export interface PublishGeneratedAudioAssetRequest {
|
||||
slot: string;
|
||||
assetKind: string;
|
||||
profileId?: string | null;
|
||||
storagePrefix?: 'puzzle_assets' | 'match3d_assets' | 'custom_world_scenes' | null;
|
||||
storagePrefix?:
|
||||
| 'puzzle_assets'
|
||||
| 'match3d_assets'
|
||||
| 'wooden_fish_assets'
|
||||
| 'custom_world_scenes'
|
||||
| null;
|
||||
}
|
||||
|
||||
export interface GeneratedAudioAssetResponse {
|
||||
|
||||
@@ -5,3 +5,4 @@ export type * from './jumpHop';
|
||||
export type * from './puzzleCreativeTemplate';
|
||||
export type * from './visualNovel';
|
||||
export type * from './barkBattle';
|
||||
export type * from './woodenFish';
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
export type WoodenFishGenerationStatus =
|
||||
| 'draft'
|
||||
| 'generating'
|
||||
| 'ready'
|
||||
| 'failed';
|
||||
|
||||
export type WoodenFishActionType =
|
||||
| 'compile-draft'
|
||||
| 'regenerate-hit-object'
|
||||
| 'generate-hit-sound'
|
||||
| 'replace-hit-sound'
|
||||
| 'update-work-meta'
|
||||
| 'update-floating-words';
|
||||
|
||||
export type WoodenFishRunStatus = 'playing' | 'finished';
|
||||
|
||||
export interface WoodenFishImageAsset {
|
||||
assetId: string;
|
||||
imageSrc: string;
|
||||
imageObjectKey: string;
|
||||
assetObjectId: string;
|
||||
generationProvider: string;
|
||||
prompt: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface WoodenFishAudioAsset {
|
||||
assetId: string;
|
||||
audioSrc: string;
|
||||
audioObjectKey: string;
|
||||
assetObjectId: string;
|
||||
source: string;
|
||||
prompt?: string | null;
|
||||
durationMs?: number | null;
|
||||
}
|
||||
|
||||
export interface WoodenFishWorkspaceCreateRequest {
|
||||
templateId: string;
|
||||
workTitle: string;
|
||||
workDescription: string;
|
||||
themeTags: string[];
|
||||
hitObjectPrompt: string;
|
||||
hitObjectReferenceImageSrc?: string | null;
|
||||
hitSoundPrompt?: string | null;
|
||||
hitSoundAsset?: WoodenFishAudioAsset | null;
|
||||
floatingWords: string[];
|
||||
}
|
||||
|
||||
export interface WoodenFishActionRequest {
|
||||
actionType: WoodenFishActionType;
|
||||
profileId?: string | null;
|
||||
workTitle?: string | null;
|
||||
workDescription?: string | null;
|
||||
themeTags?: string[] | null;
|
||||
hitObjectPrompt?: string | null;
|
||||
hitObjectReferenceImageSrc?: string | null;
|
||||
hitSoundPrompt?: string | null;
|
||||
hitSoundAsset?: WoodenFishAudioAsset | null;
|
||||
floatingWords?: string[] | null;
|
||||
}
|
||||
|
||||
export interface WoodenFishWordCounter {
|
||||
text: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface WoodenFishDraftResponse {
|
||||
templateId: string;
|
||||
templateName: string;
|
||||
profileId: string | null;
|
||||
workTitle: string;
|
||||
workDescription: string;
|
||||
themeTags: string[];
|
||||
hitObjectPrompt: string;
|
||||
hitObjectReferenceImageSrc: string | null;
|
||||
hitSoundPrompt: string | null;
|
||||
floatingWords: string[];
|
||||
hitObjectAsset: WoodenFishImageAsset | null;
|
||||
hitSoundAsset: WoodenFishAudioAsset | null;
|
||||
coverImageSrc: string | null;
|
||||
generationStatus: WoodenFishGenerationStatus;
|
||||
}
|
||||
|
||||
export interface WoodenFishSessionSnapshotResponse {
|
||||
sessionId: string;
|
||||
ownerUserId: string;
|
||||
status: WoodenFishGenerationStatus;
|
||||
draft: WoodenFishDraftResponse | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface WoodenFishSessionResponse {
|
||||
session: WoodenFishSessionSnapshotResponse;
|
||||
}
|
||||
|
||||
export interface WoodenFishActionResponse {
|
||||
actionType: WoodenFishActionType;
|
||||
session: WoodenFishSessionSnapshotResponse;
|
||||
work: WoodenFishWorkProfileResponse | null;
|
||||
}
|
||||
|
||||
export interface WoodenFishWorkSummaryResponse {
|
||||
runtimeKind: 'wooden-fish';
|
||||
workId: string;
|
||||
profileId: string;
|
||||
ownerUserId: string;
|
||||
sourceSessionId: string | null;
|
||||
workTitle: string;
|
||||
workDescription: string;
|
||||
themeTags: string[];
|
||||
coverImageSrc: string | null;
|
||||
publicationStatus: string;
|
||||
playCount: number;
|
||||
updatedAt: string;
|
||||
publishedAt: string | null;
|
||||
publishReady: boolean;
|
||||
generationStatus: WoodenFishGenerationStatus;
|
||||
}
|
||||
|
||||
export interface WoodenFishWorkProfileResponse {
|
||||
summary: WoodenFishWorkSummaryResponse;
|
||||
draft: WoodenFishDraftResponse;
|
||||
hitObjectAsset: WoodenFishImageAsset;
|
||||
hitSoundAsset: WoodenFishAudioAsset;
|
||||
floatingWords: string[];
|
||||
}
|
||||
|
||||
export interface WoodenFishWorkDetailResponse {
|
||||
item: WoodenFishWorkProfileResponse;
|
||||
}
|
||||
|
||||
export interface WoodenFishWorkMutationResponse {
|
||||
item: WoodenFishWorkProfileResponse;
|
||||
}
|
||||
|
||||
export interface WoodenFishGalleryCardResponse {
|
||||
publicWorkCode: string;
|
||||
workId: string;
|
||||
profileId: string;
|
||||
ownerUserId: string;
|
||||
authorDisplayName: string;
|
||||
workTitle: string;
|
||||
workDescription: string;
|
||||
coverImageSrc: string | null;
|
||||
themeTags: string[];
|
||||
publicationStatus: string;
|
||||
playCount: number;
|
||||
updatedAt: string;
|
||||
publishedAt: string | null;
|
||||
generationStatus: WoodenFishGenerationStatus;
|
||||
}
|
||||
|
||||
export interface WoodenFishGalleryResponse {
|
||||
items: WoodenFishGalleryCardResponse[];
|
||||
hasMore: boolean;
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
export interface WoodenFishGalleryDetailResponse {
|
||||
item: WoodenFishWorkProfileResponse;
|
||||
}
|
||||
|
||||
export interface WoodenFishRuntimeRunSnapshotResponse {
|
||||
runId: string;
|
||||
profileId: string;
|
||||
ownerUserId: string;
|
||||
status: WoodenFishRunStatus;
|
||||
totalTapCount: number;
|
||||
wordCounters: WoodenFishWordCounter[];
|
||||
startedAtMs: number;
|
||||
updatedAtMs: number;
|
||||
finishedAtMs: number | null;
|
||||
}
|
||||
|
||||
export interface WoodenFishRunResponse {
|
||||
run: WoodenFishRuntimeRunSnapshotResponse;
|
||||
}
|
||||
|
||||
export interface WoodenFishStartRunRequest {
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface WoodenFishCheckpointRunRequest {
|
||||
totalTapCount: number;
|
||||
wordCounters: WoodenFishWordCounter[];
|
||||
clientEventId: string;
|
||||
}
|
||||
|
||||
export interface WoodenFishFinishRunRequest {
|
||||
totalTapCount: number;
|
||||
wordCounters: WoodenFishWordCounter[];
|
||||
clientEventId: string;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 562 KiB |
Generated
+10
@@ -1947,6 +1947,14 @@ dependencies = [
|
||||
"spacetimedb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "module-wooden-fish"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"spacetimedb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
@@ -3278,6 +3286,7 @@ dependencies = [
|
||||
"module-square-hole",
|
||||
"module-story",
|
||||
"module-visual-novel",
|
||||
"module-wooden-fish",
|
||||
"opentelemetry",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3311,6 +3320,7 @@ dependencies = [
|
||||
"module-runtime-item",
|
||||
"module-square-hole",
|
||||
"module-story",
|
||||
"module-wooden-fish",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
|
||||
@@ -18,6 +18,7 @@ members = [
|
||||
"crates/module-inventory",
|
||||
"crates/module-custom-world",
|
||||
"crates/module-jump-hop",
|
||||
"crates/module-wooden-fish",
|
||||
"crates/module-match3d",
|
||||
"crates/module-npc",
|
||||
"crates/module-puzzle",
|
||||
@@ -59,6 +60,7 @@ module-creative-agent = { path = "crates/module-creative-agent", default-feature
|
||||
module-custom-world = { path = "crates/module-custom-world", default-features = false }
|
||||
module-inventory = { path = "crates/module-inventory", default-features = false }
|
||||
module-jump-hop = { path = "crates/module-jump-hop", default-features = false }
|
||||
module-wooden-fish = { path = "crates/module-wooden-fish", default-features = false }
|
||||
module-match3d = { path = "crates/module-match3d", default-features = false }
|
||||
module-npc = { path = "crates/module-npc", default-features = false }
|
||||
module-progression = { path = "crates/module-progression", default-features = false }
|
||||
|
||||
@@ -60,6 +60,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.merge(modules::match3d::router(state.clone()))
|
||||
.merge(modules::square_hole::router(state.clone()))
|
||||
.merge(modules::jump_hop::router(state.clone()))
|
||||
.merge(modules::wooden_fish::router(state.clone()))
|
||||
.merge(modules::puzzle::router(state.clone()))
|
||||
.merge(visual_novel_router(state.clone()))
|
||||
.route(
|
||||
|
||||
@@ -89,6 +89,7 @@ mod volcengine_speech;
|
||||
mod wechat_auth;
|
||||
mod wechat_pay;
|
||||
mod wechat_provider;
|
||||
mod wooden_fish;
|
||||
mod work_author;
|
||||
mod work_play_tracking;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use super::*;
|
||||
#[cfg(test)]
|
||||
use crate::generated_asset_sheets::crop_generated_asset_sheet_view_edge_matte;
|
||||
use crate::generated_asset_sheets::{
|
||||
GeneratedAssetSheetPersistInput, GeneratedAssetSheetPersistPrompt,
|
||||
GeneratedAssetSheetPromptInput, GeneratedAssetSheetSliceImage,
|
||||
build_generated_asset_sheet_prompt, persist_generated_asset_sheet_bytes,
|
||||
slice_generated_asset_sheet,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::generated_asset_sheets::crop_generated_asset_sheet_view_edge_matte;
|
||||
|
||||
pub(super) async fn generate_match3d_item_assets(
|
||||
state: &AppState,
|
||||
|
||||
@@ -14,3 +14,4 @@ pub mod profile;
|
||||
pub mod puzzle;
|
||||
pub mod square_hole;
|
||||
pub mod story;
|
||||
pub mod wooden_fish;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
use axum::{
|
||||
Router, middleware,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::require_bearer_auth,
|
||||
state::AppState,
|
||||
wooden_fish::{
|
||||
checkpoint_wooden_fish_run, create_wooden_fish_session, execute_wooden_fish_action,
|
||||
finish_wooden_fish_run, get_wooden_fish_gallery_detail, get_wooden_fish_runtime_work,
|
||||
get_wooden_fish_session, list_wooden_fish_gallery, publish_wooden_fish_work,
|
||||
start_wooden_fish_run,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn router(state: AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/api/creation/wooden-fish/sessions",
|
||||
post(create_wooden_fish_session).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/wooden-fish/sessions/{session_id}",
|
||||
get(get_wooden_fish_session).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/wooden-fish/sessions/{session_id}/actions",
|
||||
post(execute_wooden_fish_action).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/wooden-fish/works/{profile_id}/publish",
|
||||
post(publish_wooden_fish_work).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/wooden-fish/works/{profile_id}",
|
||||
get(get_wooden_fish_runtime_work),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/wooden-fish/runs",
|
||||
post(start_wooden_fish_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/wooden-fish/runs/{run_id}/checkpoint",
|
||||
post(checkpoint_wooden_fish_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/wooden-fish/runs/{run_id}/finish",
|
||||
post(finish_wooden_fish_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/wooden-fish/gallery",
|
||||
get(list_wooden_fish_gallery),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/wooden-fish/gallery/{public_work_code}",
|
||||
get(get_wooden_fish_gallery_detail),
|
||||
)
|
||||
}
|
||||
@@ -1258,9 +1258,7 @@ fn build_admin_runtime(
|
||||
#[cfg(debug_assertions)]
|
||||
fn is_missing_creation_entry_config_procedure(error: &SpacetimeClientError) -> bool {
|
||||
match error {
|
||||
SpacetimeClientError::Procedure(message) => {
|
||||
message.contains("No such procedure")
|
||||
}
|
||||
SpacetimeClientError::Procedure(message) => message.contains("No such procedure"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,13 +233,15 @@ pub async fn create_visual_novel_sound_effect_task(
|
||||
}
|
||||
|
||||
pub async fn create_sound_effect_task(
|
||||
State(_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Extension(request_context): axum::extract::Extension<RequestContext>,
|
||||
payload: Result<Json<creation_audio::CreateSoundEffectRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let _ = parse_json_payload(&request_context, payload)?;
|
||||
Err(creation_audio_generation_disabled_error()
|
||||
.into_response_with_context(Some(&request_context)))
|
||||
let Json(payload) = parse_json_payload(&request_context, payload)?;
|
||||
create_sound_effect_task_response(&state, payload.prompt, payload.duration, payload.seed)
|
||||
.await
|
||||
.map(|task| json_success_body(Some(&request_context), task))
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))
|
||||
}
|
||||
|
||||
pub(crate) async fn generate_sound_effect_asset_for_creation(
|
||||
@@ -518,15 +520,25 @@ pub async fn publish_background_music_asset(
|
||||
}
|
||||
|
||||
pub async fn publish_sound_effect_asset(
|
||||
State(_state): State<AppState>,
|
||||
Path(_task_id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
Path(task_id): Path<String>,
|
||||
axum::extract::Extension(request_context): axum::extract::Extension<RequestContext>,
|
||||
axum::extract::Extension(_authenticated): axum::extract::Extension<AuthenticatedAccessToken>,
|
||||
axum::extract::Extension(authenticated): axum::extract::Extension<AuthenticatedAccessToken>,
|
||||
payload: Result<Json<creation_audio::PublishGeneratedAudioAssetRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let payload = parse_json_payload(&request_context, payload)?.0;
|
||||
Err(creation_audio_generation_disabled_error_for_target(payload)
|
||||
.into_response_with_context(Some(&request_context)))
|
||||
let target = build_creation_audio_target(payload, AudioAssetSlot::SoundEffect)
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
|
||||
publish_generated_audio_asset(
|
||||
&state,
|
||||
authenticated.claims().user_id(),
|
||||
task_id,
|
||||
AudioAssetSlot::SoundEffect,
|
||||
target,
|
||||
)
|
||||
.await
|
||||
.map(|payload| json_success_body(Some(&request_context), payload))
|
||||
.map_err(|error| error.into_response_with_context(Some(&request_context)))
|
||||
}
|
||||
|
||||
async fn publish_generated_audio_asset(
|
||||
@@ -860,10 +872,36 @@ fn build_visual_novel_audio_target(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_creation_audio_target(
|
||||
payload: creation_audio::PublishGeneratedAudioAssetRequest,
|
||||
slot: AudioAssetSlot,
|
||||
) -> Result<AudioAssetBindingTarget, AppError> {
|
||||
if matches!(slot, AudioAssetSlot::SoundEffect)
|
||||
&& payload.entity_kind.trim() == "wooden_fish_work"
|
||||
&& payload.slot.trim() == "hit_sound"
|
||||
&& payload.asset_kind.trim() == "wooden_fish_hit_sound"
|
||||
&& payload.storage_prefix
|
||||
== Some(creation_audio::CreationAudioStoragePrefix::WoodenFishAssets)
|
||||
{
|
||||
let entity_id = normalize_limited_text(&payload.entity_id, "entityId", 160)?;
|
||||
return Ok(AudioAssetBindingTarget {
|
||||
storage_scope: payload.entity_kind.trim().to_string(),
|
||||
entity_kind: payload.entity_kind.trim().to_string(),
|
||||
entity_id,
|
||||
slot: payload.slot.trim().to_string(),
|
||||
asset_kind: payload.asset_kind.trim().to_string(),
|
||||
profile_id: normalize_optional_text(payload.profile_id.as_deref()),
|
||||
storage_prefix: LegacyAssetPrefix::WoodenFishAssets,
|
||||
});
|
||||
}
|
||||
|
||||
Err(creation_audio_generation_disabled_error_for_target(payload))
|
||||
}
|
||||
|
||||
fn creation_audio_generation_disabled_error() -> AppError {
|
||||
AppError::from_status(StatusCode::GONE).with_details(json!({
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": "拼图与抓大鹅音频生成入口已临时关闭",
|
||||
"message": "当前创作音频目标未开放",
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -872,8 +910,9 @@ fn creation_audio_generation_disabled_error_for_target(
|
||||
) -> AppError {
|
||||
creation_audio_generation_disabled_error().with_details(json!({
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": "拼图与抓大鹅音频生成入口已临时关闭",
|
||||
"message": "当前创作音频目标未开放",
|
||||
"entityKind": payload.entity_kind.trim(),
|
||||
"slot": payload.slot.trim(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1434,7 +1473,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_creation_audio_targets_return_gone() {
|
||||
fn disabled_creation_audio_targets_return_gone_except_wooden_fish_sound_effects() {
|
||||
let payload = creation_audio::PublishGeneratedAudioAssetRequest {
|
||||
entity_kind: "puzzle_work".to_string(),
|
||||
entity_id: "puzzle-profile-1".to_string(),
|
||||
@@ -1467,6 +1506,22 @@ mod tests {
|
||||
};
|
||||
let error = creation_audio_generation_disabled_error_for_target(payload);
|
||||
assert_eq!(error.status_code(), StatusCode::GONE);
|
||||
|
||||
let payload = creation_audio::PublishGeneratedAudioAssetRequest {
|
||||
entity_kind: "wooden_fish_work".to_string(),
|
||||
entity_id: "wooden-fish-profile-1".to_string(),
|
||||
slot: "hit_sound".to_string(),
|
||||
asset_kind: "wooden_fish_hit_sound".to_string(),
|
||||
profile_id: Some("wooden-fish-profile-1".to_string()),
|
||||
storage_prefix: Some(creation_audio::CreationAudioStoragePrefix::WoodenFishAssets),
|
||||
};
|
||||
let target = build_creation_audio_target(payload, AudioAssetSlot::SoundEffect)
|
||||
.expect("wooden fish hit sound target should be enabled");
|
||||
|
||||
assert_eq!(target.entity_kind, "wooden_fish_work");
|
||||
assert_eq!(target.slot, "hit_sound");
|
||||
assert_eq!(target.storage_prefix, LegacyAssetPrefix::WoodenFishAssets);
|
||||
assert_eq!(target.storage_scope, "wooden_fish_work");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -348,15 +348,15 @@ mod tests {
|
||||
assert_eq!(perfect.status, JumpHopRunStatus::Playing);
|
||||
assert_eq!(perfect.current_platform_index, 1);
|
||||
|
||||
let hit = apply_jump(&run, perfect_charge.saturating_add(80), 200)
|
||||
.expect("jump should resolve");
|
||||
let hit =
|
||||
apply_jump(&run, perfect_charge.saturating_add(80), 200).expect("jump should resolve");
|
||||
assert_eq!(
|
||||
hit.last_jump.as_ref().unwrap().result,
|
||||
JumpHopJumpResultKind::Hit
|
||||
);
|
||||
|
||||
let miss = apply_jump(&run, perfect_charge.saturating_add(900), 200)
|
||||
.expect("jump should resolve");
|
||||
let miss =
|
||||
apply_jump(&run, perfect_charge.saturating_add(900), 200).expect("jump should resolve");
|
||||
assert_eq!(miss.status, JumpHopRunStatus::Failed);
|
||||
assert_eq!(
|
||||
miss.last_jump.as_ref().unwrap().result,
|
||||
|
||||
@@ -105,6 +105,17 @@ pub fn default_creation_entry_type_snapshots(
|
||||
45,
|
||||
updated_at_micros,
|
||||
),
|
||||
build_default_creation_entry_type_snapshot(
|
||||
"wooden-fish",
|
||||
"敲木鱼",
|
||||
"点击祈福轻玩法",
|
||||
"可创建",
|
||||
"/wooden-fish/default-hit-object.png",
|
||||
true,
|
||||
true,
|
||||
47,
|
||||
updated_at_micros,
|
||||
),
|
||||
build_default_creation_entry_type_snapshot(
|
||||
"square-hole",
|
||||
"方洞",
|
||||
|
||||
@@ -251,6 +251,25 @@ mod tests {
|
||||
assert_eq!(bark_battle.sort_order, 85);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_creation_entry_types_include_wooden_fish() {
|
||||
let configs = default_creation_entry_type_snapshots(1);
|
||||
let wooden_fish = configs
|
||||
.iter()
|
||||
.find(|item| item.id == "wooden-fish")
|
||||
.expect("wooden-fish creation entry should be seeded");
|
||||
|
||||
assert_eq!(wooden_fish.title, "敲木鱼");
|
||||
assert!(wooden_fish.visible);
|
||||
assert!(wooden_fish.open);
|
||||
assert_eq!(wooden_fish.badge, "可创建");
|
||||
assert_eq!(wooden_fish.sort_order, 47);
|
||||
assert_eq!(
|
||||
wooden_fish.image_src,
|
||||
"/wooden-fish/default-hit-object.png"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalized_clamps_music_volume_into_valid_range() {
|
||||
let low = RuntimeSettings::normalized(-1.0, RuntimePlatformTheme::Light);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "module-wooden-fish"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
spacetime-types = ["dep:spacetimedb"]
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
spacetimedb = { workspace = true, optional = true }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user