1
This commit is contained in:
@@ -82,7 +82,7 @@
|
||||
5. 玩家从广场进入某个作品时,第 1 关必须先显示当前作品本身。
|
||||
6. 第 2 关及以后必须按照“标签相似度权重 `70%` + 同作者权重 `30%`”选择下一关。
|
||||
7. 游戏运行时必须全屏展示拼图画布。
|
||||
8. 新游戏进入时难度必须从 `3*3` 开始,完成 `3` 关后切为 `4*4`,后续持续为 `4*4`。
|
||||
8. 新游戏进入时难度必须从第 `1` 关的 `3*3` 开始,并按关卡配置推进到 `4*4`、`5*5`、`6*6`、`7*7`;第 `11` 关起每 `6` 关循环复用第 `5~10` 关配置。
|
||||
9. 拼图运行时必须支持:
|
||||
- 点击选择两块并交换
|
||||
- 正确相邻后自动合并
|
||||
@@ -517,21 +517,35 @@ tagSimilarityScore =
|
||||
本次建议同时显示:
|
||||
|
||||
1. 当前关卡序号
|
||||
2. 当前网格规格,例如 `3x3` 或 `4x4`
|
||||
2. 当前网格规格,例如 `3x3`、`5x5` 或 `7x7`
|
||||
|
||||
## 9.3 难度与关卡推进规则
|
||||
|
||||
每次新 run 都必须从最低难度开始:
|
||||
每次新 run 都必须从第 `1` 关配置开始:
|
||||
|
||||
1. 第 `1~3` 关固定为 `3x3`
|
||||
2. 第 `4` 关开始固定为 `4x4`
|
||||
3. 后续全部关卡保持 `4x4`
|
||||
| 关卡 | 切割规格 | 限时 |
|
||||
| ---------- | -------- | -------------- |
|
||||
| 第 `1` 关 | `3x3` | `5` 分钟 |
|
||||
| 第 `2` 关 | `4x4` | `5` 分钟 |
|
||||
| 第 `3` 关 | `5x5` | `5` 分钟 |
|
||||
| 第 `4` 关 | `5x5` | `3` 分 `30` 秒 |
|
||||
| 第 `5` 关 | `5x5` | `3` 分 `30` 秒 |
|
||||
| 第 `6` 关 | `6x6` | `4` 分钟 |
|
||||
| 第 `7` 关 | `5x5` | `3` 分 `30` 秒 |
|
||||
| 第 `8` 关 | `7x7` | `4` 分 `30` 秒 |
|
||||
| 第 `9` 关 | `5x5` | `4` 分钟 |
|
||||
| 第 `10` 关 | `7x7` | `4` 分 `30` 秒 |
|
||||
|
||||
第 `11` 关开始,每 `6` 关循环复用第 `5~10` 关配置。
|
||||
|
||||
对应函数建议:
|
||||
|
||||
```ts
|
||||
function resolvePuzzleGridSize(clearedLevelCount: number): 3 | 4 {
|
||||
return clearedLevelCount >= 3 ? 4 : 3;
|
||||
function resolvePuzzleLevelConfig(levelIndex: number): {
|
||||
gridSize: 3 | 4 | 5 | 6 | 7;
|
||||
timeLimitMs: number;
|
||||
} {
|
||||
// 统一从关卡序号解析切割规格和倒计时。
|
||||
}
|
||||
```
|
||||
|
||||
@@ -646,8 +660,8 @@ V1 规则如下:
|
||||
|
||||
`2026-04-29` 起,拼图运行时加入倒计时:
|
||||
|
||||
1. `3x3` 关卡限时 `180` 秒。
|
||||
2. `4x4` 关卡限时 `300` 秒。
|
||||
1. 倒计时必须使用第 `9.3` 节的关卡配置函数,不允许在 UI 或本地兜底里按网格规模另写一套时间表。
|
||||
2. 第 `1~10` 关按配置表执行;第 `11` 关起每 `6` 关循环复用第 `5~10` 关配置。
|
||||
3. 规定时间内未完成拼图,关卡状态变为 `failed`。
|
||||
4. 弹窗、查看原图覆盖、冻结时间生效期间不消耗倒计时。
|
||||
5. 通关成绩只统计有效消耗时间,不统计暂停与冻结时间。
|
||||
@@ -693,7 +707,7 @@ interface PuzzleProfile {
|
||||
interface PuzzleRuntimeLevelSnapshot {
|
||||
runId: string;
|
||||
levelIndex: number;
|
||||
gridSize: 3 | 4;
|
||||
gridSize: 3 | 4 | 5 | 6 | 7;
|
||||
profileId: string;
|
||||
levelName: string;
|
||||
authorDisplayName: string;
|
||||
@@ -738,7 +752,7 @@ interface PuzzleRunSnapshot {
|
||||
entryProfileId: string;
|
||||
clearedLevelCount: number;
|
||||
currentLevelIndex: number;
|
||||
currentGridSize: 3 | 4;
|
||||
currentGridSize: 3 | 4 | 5 | 6 | 7;
|
||||
playedProfileIds: string[];
|
||||
previousLevelTags: string[];
|
||||
currentLevel: PuzzleRuntimeLevelSnapshot | null;
|
||||
@@ -1167,7 +1181,7 @@ interface PuzzleRunSnapshot {
|
||||
|
||||
先做:
|
||||
|
||||
1. `3x3 / 4x4` 切图
|
||||
1. `3x3 / 4x4 / 5x5 / 6x6 / 7x7` 切图
|
||||
2. 点击两块交换
|
||||
3. 正确连接自动合并
|
||||
4. 合并块整体拖动
|
||||
@@ -1202,7 +1216,7 @@ interface PuzzleRunSnapshot {
|
||||
4. 发布后的拼图作品能进入平台广场。
|
||||
5. 玩家从广场进入时,第 `1` 关必定是当前作品本身。
|
||||
6. 第 `2` 关及以后按照“标签相似度 `70%` + 同作者 `30%`”计算下一关。
|
||||
7. 新 run 前 `3` 关为 `3x3`,之后固定为 `4x4`。
|
||||
7. 新 run 的关卡切割和倒计时符合第 `9.3` 节配置,并且第 `11` 关起按第 `5~10` 关配置循环。
|
||||
8. 运行时支持点击两块交换。
|
||||
9. 交换后正确相邻的块会自动合并。
|
||||
10. 合并块可以整体拖动。
|
||||
|
||||
@@ -93,8 +93,9 @@
|
||||
|
||||
1. 数字过大时做单位缩略展示
|
||||
2. “游戏时长”卡固定以小时为单位展示,短时长不切换成分钟,长时长不切换成天
|
||||
3. 进入页面先展示骨架屏
|
||||
4. 数据请求失败时展示降级文案,不展示假数字
|
||||
3. “玩过”卡展示值始终带 `个` 单位,例如 `0个`、`1个`、`1.2万个`
|
||||
4. 进入页面先展示骨架屏
|
||||
5. 数据请求失败时展示降级文案,不展示假数字
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
1. 拼图生成图固定使用 `1024*1024`。
|
||||
2. 文生图和参考图生图共用同一个尺寸常量,禁止一条链路仍生成竖屏或横版图。
|
||||
3. 拼图图片提示词明确写入 `1:1 正方形画布`,继续保留 `3x3 或 4x4 拼图切块`、主体清晰、层次明确、无文字水印等约束。
|
||||
3. 拼图图片提示词明确写入 `1:1 正方形画布`,继续保留适配 `3x3 / 4x4 / 5x5 / 6x6 / 7x7` 拼图切块、主体清晰、层次明确、无文字水印等约束。
|
||||
4. 文生图正向 prompt 必须由后端压缩到 `500` 字符以内,优先保留玩家画面描述开头与固定拼图约束,避免 DashScope 旧 text2image 协议把超长 prompt 判为“请求参数不合法”。
|
||||
5. DashScope 上游失败时,api-server 必须在错误 details 中保留业务 message、`upstreamStatus` 和截断后的 `rawExcerpt`,日志也要记录同样的摘要,避免生成进度页只能看到通用 HTTP 文案。
|
||||
6. 图片生成仍由 `api-server` 执行。SpacetimeDB reducer 不做网络 I/O。
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
1. 点击拼图草稿生成或重新生成画面时,后端请求 DashScope 的 `size` 为 `1024*1024`。
|
||||
2. 图片提示词包含 `1:1 正方形拼图关卡`。
|
||||
3. 图片提示词长度不超过 `500` 字符,超长画面描述会被截断,但 `3x3 或 4x4`、`避免文字、水印、边框和 UI 元素` 等玩法约束不能丢。
|
||||
3. 图片提示词长度不超过 `500` 字符,超长画面描述会被截断,但适配 `3x3 / 4x4 / 5x5 / 6x6 / 7x7` 拼图切块、`避免文字、水印、边框和 UI 元素` 等玩法约束不能丢。
|
||||
4. DashScope 返回参数错误、任务失败或非 2xx 时,前端错误优先展示后端 details.message,后端日志能看到 `upstreamStatus` 和 `rawExcerpt`。
|
||||
5. 正式拼图 run 中拖动拼块后,前端立即更新棋盘、合并块和通关状态,不再等待 `/drag`。
|
||||
6. 移动端运行时棋盘为正方形,并尽量贴近屏幕两侧边缘。
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
1. 通关后默认点击“下一关”,优先加载当前拼图作品的下一关。
|
||||
2. 当前作品没有下一关时,后端按标签语义相似度选出相似度最高的三个已发布作品。
|
||||
3. 用户在通关弹窗里点击候选作品后,进入该作品并从第 1 关重新开始。
|
||||
3. 用户在通关弹窗里点击候选作品后,进入该作品并从第 `1` 关重新开始。
|
||||
4. 移动端优先,候选卡片要紧凑,不写玩法说明类文案。
|
||||
|
||||
## 数据契约
|
||||
@@ -51,8 +51,8 @@
|
||||
- 返回最高的 3 个候选
|
||||
4. `advance_puzzle_next_level`:
|
||||
- `nextLevelMode = sameWork` 时加载当前作品的下一关,并继续当前 run。
|
||||
- `nextLevelMode = similarWorks` 时默认加载候选第一项,并从该作品第 1 关重新开始。
|
||||
5. `local-next-level` 兼容接口同样优先找同作品下一关;没有时才返回相似作品候选或旧草稿兜底。
|
||||
- `nextLevelMode = similarWorks` 时默认加载候选第一项,并把 `entryProfileId / clearedLevelCount / currentLevelIndex` 重置到目标作品第 `1` 关。
|
||||
5. `local-next-level` 兼容接口同样优先找同作品下一关;没有时返回 `similarWorks` 候选并保持当前通关 run,只有候选池为空时才进入旧草稿兜底。
|
||||
|
||||
## 前端规则
|
||||
|
||||
@@ -64,11 +64,12 @@
|
||||
- `sameWork` 保留“下一关”。
|
||||
- `similarWorks` 显示“换个作品”,点击后打开结算弹窗供选择。
|
||||
3. 所有正式相似度计算只信任后端返回,不在 UI 里重新算。
|
||||
4. 本地/草稿 run 通关提交本地排行榜后,会异步调用 `local-next-level` 刷新 handoff;若拿到 `similarWorks`,只合并候选字段,不把已通关弹窗改成新的 playing 关卡。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 当前作品有下一关时,点击“下一关”进入当前作品下一关。
|
||||
2. 当前作品没有下一关时,通关弹窗显示最多 3 个相似作品。
|
||||
3. 点击相似作品后进入该作品第 1 关。
|
||||
3. 点击相似作品后进入该作品第 `1` 关,HUD 关卡序号、切割规格和倒计时都按第 `1` 关显示。
|
||||
4. 旧 `recommendedNextProfileId` 为空时,只要 `nextLevelMode = sameWork`,按钮仍可用。
|
||||
5. 拼图 runtime 单测、Rust 拼图模块测试和编码检查通过。
|
||||
|
||||
@@ -24,12 +24,28 @@
|
||||
|
||||
## 难度限时
|
||||
|
||||
第一版按网格规模定义限时:
|
||||
拼图关卡切割规格和倒计时由统一关卡配置函数解析,不再按网格规模单独推导时间:
|
||||
|
||||
1. `3x3`:`180000ms`。
|
||||
2. `4x4`:`300000ms`。
|
||||
| 关卡 | 切割规格 | 限时 |
|
||||
| -------- | -------- | ---------- |
|
||||
| 第 1 关 | `3x3` | `300000ms` |
|
||||
| 第 2 关 | `4x4` | `300000ms` |
|
||||
| 第 3 关 | `5x5` | `300000ms` |
|
||||
| 第 4 关 | `5x5` | `210000ms` |
|
||||
| 第 5 关 | `5x5` | `210000ms` |
|
||||
| 第 6 关 | `6x6` | `240000ms` |
|
||||
| 第 7 关 | `5x5` | `210000ms` |
|
||||
| 第 8 关 | `7x7` | `270000ms` |
|
||||
| 第 9 关 | `5x5` | `240000ms` |
|
||||
| 第 10 关 | `7x7` | `270000ms` |
|
||||
|
||||
后续若扩展更多难度,只能通过同一个难度解析函数扩展,不允许在 UI 里写死另一套时间。
|
||||
第 11 关开始,每 6 关循环复用第 5 关到第 10 关的配置,即 `5x5/210000ms`、`6x6/240000ms`、`5x5/210000ms`、`7x7/270000ms`、`5x5/240000ms`、`7x7/270000ms`。
|
||||
|
||||
同作品下一关必须使用同一个运行时关卡序号继续推进。跨作品相似推荐代表进入新作品,必须从目标作品第 `1` 关重新开始。
|
||||
|
||||
失败状态点击“重新开始”时,不进入作品第 `1` 关,而是重开当前失败关卡:前端需要传当前关 `levelId`,服务端按该 `levelId` 在作品内的位置恢复 `currentLevelIndex`、切割规格和倒计时。
|
||||
|
||||
后续若扩展更多难度,只能通过同一个关卡配置解析函数扩展,不允许在 UI 里写死另一套时间。
|
||||
|
||||
## 计时规则
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# 拼图作品积分激励链路设计
|
||||
|
||||
更新时间:`2026-05-01`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
1. 拼图草稿页“新增关卡”按钮下方显示一行小字:“获得更多积分激励”。
|
||||
2. 创作页的已发布拼图作品卡展示当前作品的积分激励总数、待领取积分数和领取按钮。
|
||||
3. 用户在他人已发布拼图作品中消耗陶泥币时,作品作者获得消耗陶泥币数量的一半作为积分激励。
|
||||
4. 作者领取时只能领取整数个陶泥币,待领取值向下取整;未满 1 个陶泥币的半数余额继续保留。
|
||||
|
||||
## 2. 数据模型
|
||||
|
||||
拼图作品激励归属到 `puzzle_work_profile`。
|
||||
|
||||
1. `point_incentive_total_half_points: u64`
|
||||
- 记录该作品累计获得的激励,单位为“半个陶泥币”。
|
||||
- 每消耗 `N` 个陶泥币,增加 `N` 个 half points;当前拼图道具每次消耗 1 个陶泥币,因此每次为作者增加 0.5。
|
||||
2. `point_incentive_claimed_points: u64`
|
||||
- 记录作者已领取的整数陶泥币数量。
|
||||
3. 前端展示:
|
||||
- 激励总数 = `pointIncentiveTotalHalfPoints / 2`,允许展示一位小数。
|
||||
- 待领取积分 = `floor(pointIncentiveTotalHalfPoints / 2) - pointIncentiveClaimedPoints`。
|
||||
- 领取按钮仅在待领取积分大于 0 时可用。
|
||||
|
||||
## 3. 后端事务
|
||||
|
||||
1. 拼图运行道具扣费成功、道具效果成功落库后,后端根据 run 的当前作品 `profile_id` 查找作者。
|
||||
2. 若使用者不是作品作者,则给该作品累积 `consumed_points` 个 half points。
|
||||
3. 若使用者是作者本人,视为作者自测,不产生积分激励。
|
||||
4. 若后续业务操作失败并触发扣费退款,不写入激励。
|
||||
5. 领取接口:
|
||||
- 只允许作品作者领取。
|
||||
- 计算可领取整数 `claimable = total_half_points / 2 - claimed_points`。
|
||||
- `claimable <= 0` 时拒绝领取。
|
||||
- 同一事务内更新作品 `claimed_points += claimable`,并向作者钱包增加 `claimable` 陶泥币,钱包流水来源使用 `puzzle_author_incentive_claim`。
|
||||
|
||||
## 4. API 与前端
|
||||
|
||||
1. `PuzzleWorkSummary` / `PuzzleWorkProfile` 增加:
|
||||
- `pointIncentiveTotalHalfPoints`
|
||||
- `pointIncentiveClaimedPoints`
|
||||
- `pointIncentiveTotalPoints`
|
||||
- `pointIncentiveClaimablePoints`
|
||||
2. 新增领取接口:
|
||||
- `POST /api/runtime/puzzle/works/{profile_id}/point-incentive/claim`
|
||||
- 返回更新后的 `PuzzleWorkProfile`。
|
||||
3. 创作页仅对已发布拼图作品显示积分激励块;RPG、大鱼和草稿卡不显示。
|
||||
4. 领取成功后刷新对应拼图作品列表状态,按钮立即禁用或显示新的待领取数。
|
||||
5. `spacetime-client` 映射层继续兼容历史拼图运行快照:旧 `run_json` 若缺少 `started_at_ms`,API 记录回填为非 0 值,避免前端计时器拿到无效开始时间。
|
||||
|
||||
## 5. 验收点
|
||||
|
||||
1. 拼图草稿页新增关卡按钮下方显示“获得更多积分激励”。
|
||||
2. 已发布拼图作品卡展示“积分激励总数”和“待领取”两个数值。
|
||||
3. 待领取积分为 0 时领取按钮禁用。
|
||||
4. 非作者游玩他人拼图并使用付费道具后,该作品累计 half points 增加。
|
||||
5. 作者领取后钱包增加向下取整后的整数陶泥币,作品待领取数归零或保留不足 1 的小数余额。
|
||||
6. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。
|
||||
@@ -24,8 +24,9 @@
|
||||
- [PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md](./PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md):记录拼图生成图片回到 1:1,运行时拖动、交换、合并与拆分由前端即时裁决,以及移动端棋盘贴近屏幕边缘的落地边界。
|
||||
- [PUZZLE_FORM_CREATION_FLOW_2026-04-29.md](./PUZZLE_FORM_CREATION_FLOW_2026-04-29.md):冻结拼图填表式创作入口、初始表单自动保存草稿、生成前退出后的表单恢复,以及草稿编译/首图生成的前后端边界。
|
||||
- [PUZZLE_LEADERBOARD_FRONTEND_LEVEL_AND_RPG_COMING_SOON_2026-04-30.md](./PUZZLE_LEADERBOARD_FRONTEND_LEVEL_AND_RPG_COMING_SOON_2026-04-30.md):记录拼图第二关排行榜提交以前端当前关卡为准、不被 SpacetimeDB 旧 run 快照误杀,以及 RPG 创作入口改为敬请期待的落地边界。
|
||||
- [PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md](./PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md):记录拼图通关后优先同作品下一关、无下一关时按 RPG/build 标签语义相似度返回三个候选作品并从第 1 关接续的落地规则。
|
||||
- [PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md](./PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md):记录拼图通关后优先同作品下一关、无下一关时按 RPG/build 标签语义相似度返回三个候选作品,并在跨作品时只切换到候选作品第 1 张图、运行时关卡序号继续累进的落地规则。
|
||||
- [PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md](./PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md):记录拼图失败后重新开始/付费续时,以及进入作品与过关后同步存档页投影的落地规则。
|
||||
- [PUZZLE_RUNTIME_TIMER_AND_PROPS_2026-04-29.md](./PUZZLE_RUNTIME_TIMER_AND_PROPS_2026-04-29.md):记录拼图关卡切割、倒计时、失败态和三个运行时道具的统一规则;2026-05-01 起关卡切割与限时按第 1-10 关配置,并从第 11 关按第 5-10 关六关循环。
|
||||
- [RPG_SCENE_ACT_PREVIEW_BOOTSTRAP_FIX_2026-04-30.md](./RPG_SCENE_ACT_PREVIEW_BOOTSTRAP_FIX_2026-04-30.md):记录编辑器幕预览卡在“正在载入这一幕”时的启动态根因,收口预览本地运行态装配与禁持久化首段 story 注入。
|
||||
- [PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md](./PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md):记录拼图结果页名称与标签编辑自动保存、发布门槛统一到 `3~6` 标签,以及前端发布校验不再被旧 session blocker 卡死的修复口径。
|
||||
- [WORK_AUTHOR_ID_RESOLUTION_2026-04-30.md](./WORK_AUTHOR_ID_RESOLUTION_2026-04-30.md):记录作品作者以 `owner_user_id` 为真相源,API 按用户 ID 解析最新昵称与公开用户码,历史 `author_display_name` 仅作为兼容回退。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type PuzzleGridSize = 3 | 4;
|
||||
export type PuzzleGridSize = 3 | 4 | 5 | 6 | 7;
|
||||
|
||||
export interface PuzzleCellPosition {
|
||||
row: number;
|
||||
|
||||
@@ -23,6 +23,10 @@ export interface PuzzleWorkSummary {
|
||||
remixCount?: number;
|
||||
likeCount?: number;
|
||||
recentPlayCount7d?: number;
|
||||
pointIncentiveTotalHalfPoints?: number;
|
||||
pointIncentiveClaimedPoints?: number;
|
||||
pointIncentiveTotalPoints?: number;
|
||||
pointIncentiveClaimablePoints?: number;
|
||||
publishReady: boolean;
|
||||
levels?: PuzzleDraftLevel[];
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ export type ProfileWalletLedgerEntry = {
|
||||
| 'points_recharge'
|
||||
| 'asset_operation_consume'
|
||||
| 'asset_operation_refund'
|
||||
| 'redeem_code_reward';
|
||||
| 'redeem_code_reward'
|
||||
| 'puzzle_author_incentive_claim';
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
|
||||
+18
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
{"choices":[{"message":{"content":"{\"name\":\"雾港归航\",\"subtitle\":\"失灯旧案\",\"summary\":\"守灯人与群岛议会围绕沉船旧案对峙。\",\"tone\":\"海雾悬疑\",\"playerGoal\":\"查清父亲沉船真相\",\"templateWorldType\":\"WUXIA\",\"majorFactions\":[\"群岛议会\",\"灯塔署\"],\"coreConflicts\":[\"守灯塔的旧档案被人改写。\"],\"attributeSchema\":{\"slots\":[{\"name\":\"灯骨\"},{\"name\":\"潮步\"},{\"name\":\"灯识\"},{\"name\":\"雾魄\"},{\"name\":\"旧约\"},{\"name\":\"回澜\"}]},\"camp\":{\"name\":\"旧灯塔归舍\",\"description\":\"海雾边缘的守灯人旧居。\"}}"}}],"id":"resp_01"}
|
||||
@@ -83,13 +83,13 @@ use crate::{
|
||||
phone_auth::{phone_login, send_phone_code},
|
||||
profile_identity::update_profile_identity,
|
||||
puzzle::{
|
||||
advance_local_puzzle_next_level, advance_puzzle_next_level, create_puzzle_agent_session,
|
||||
delete_puzzle_work, execute_puzzle_agent_action, get_puzzle_agent_session,
|
||||
get_puzzle_gallery_detail, get_puzzle_run, get_puzzle_work_detail, get_puzzle_works,
|
||||
list_puzzle_gallery, put_puzzle_work, record_puzzle_gallery_like,
|
||||
remix_puzzle_gallery_work, start_puzzle_run, stream_puzzle_agent_message,
|
||||
submit_puzzle_agent_message, submit_puzzle_leaderboard, swap_puzzle_pieces,
|
||||
update_puzzle_run_pause, use_puzzle_runtime_prop,
|
||||
advance_local_puzzle_next_level, advance_puzzle_next_level,
|
||||
claim_puzzle_work_point_incentive, create_puzzle_agent_session, delete_puzzle_work,
|
||||
execute_puzzle_agent_action, get_puzzle_agent_session, get_puzzle_gallery_detail,
|
||||
get_puzzle_run, get_puzzle_work_detail, get_puzzle_works, list_puzzle_gallery,
|
||||
put_puzzle_work, record_puzzle_gallery_like, remix_puzzle_gallery_work, start_puzzle_run,
|
||||
stream_puzzle_agent_message, submit_puzzle_agent_message, submit_puzzle_leaderboard,
|
||||
swap_puzzle_pieces, update_puzzle_run_pause, use_puzzle_runtime_prop,
|
||||
},
|
||||
refresh_session::refresh_session,
|
||||
request_context::{attach_request_context, resolve_request_id},
|
||||
@@ -764,6 +764,13 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/works/{profile_id}/point-incentive/claim",
|
||||
post(claim_puzzle_work_point_incentive).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route("/api/runtime/puzzle/gallery", get(list_puzzle_gallery))
|
||||
.route(
|
||||
"/api/runtime/puzzle/gallery/{profile_id}",
|
||||
|
||||
@@ -17,7 +17,10 @@ use module_assets::{
|
||||
AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input,
|
||||
build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id,
|
||||
};
|
||||
use module_puzzle::{PuzzleBoardSnapshot, PuzzleGeneratedImageCandidate, PuzzleRuntimeLevelStatus};
|
||||
use module_puzzle::{
|
||||
PuzzleBoardSnapshot, PuzzleGeneratedImageCandidate, PuzzleRuntimeLevelStatus,
|
||||
PuzzleWorkProfile, resolve_puzzle_level_config,
|
||||
};
|
||||
use platform_oss::{
|
||||
LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest,
|
||||
OssSignedGetObjectUrlRequest,
|
||||
@@ -61,8 +64,9 @@ use spacetime_client::{
|
||||
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
|
||||
PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord,
|
||||
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord,
|
||||
PuzzleSelectCoverImageRecordInput, PuzzleWorkLikeReportRecordInput, PuzzleWorkProfileRecord,
|
||||
PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput, SpacetimeClientError,
|
||||
PuzzleSelectCoverImageRecordInput, PuzzleWorkLikeReportRecordInput,
|
||||
PuzzleWorkPointIncentiveClaimRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput,
|
||||
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
|
||||
};
|
||||
use std::convert::Infallible;
|
||||
use tokio::time::sleep;
|
||||
@@ -966,6 +970,43 @@ pub async fn delete_puzzle_work(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn claim_puzzle_work_point_incentive(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(profile_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_WORKS_PROVIDER,
|
||||
&profile_id,
|
||||
"profileId",
|
||||
)?;
|
||||
|
||||
let item = state
|
||||
.spacetime_client()
|
||||
.claim_puzzle_work_point_incentive(PuzzleWorkPointIncentiveClaimRecordInput {
|
||||
profile_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
claimed_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_WORKS_PROVIDER,
|
||||
map_puzzle_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
PuzzleWorkMutationResponse {
|
||||
item: map_puzzle_work_profile_response(&state, item),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_puzzle_gallery(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -1370,6 +1411,7 @@ pub async fn use_puzzle_runtime_prop(
|
||||
owner_user_id: reducer_owner_user_id,
|
||||
prop_kind,
|
||||
used_at_micros: current_utc_micros(),
|
||||
spent_points: crate::asset_billing::ASSET_OPERATION_POINTS_COST,
|
||||
})
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)
|
||||
@@ -1689,6 +1731,13 @@ fn map_puzzle_work_summary_response(
|
||||
remix_count: item.remix_count,
|
||||
like_count: item.like_count,
|
||||
recent_play_count_7d: item.recent_play_count_7d,
|
||||
point_incentive_total_half_points: item.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: item.point_incentive_claimed_points,
|
||||
point_incentive_total_points: item.point_incentive_total_half_points as f64 / 2.0,
|
||||
point_incentive_claimable_points: item
|
||||
.point_incentive_total_half_points
|
||||
.saturating_div(2)
|
||||
.saturating_sub(item.point_incentive_claimed_points),
|
||||
publish_ready: item.publish_ready,
|
||||
levels: Vec::new(),
|
||||
}
|
||||
@@ -1898,7 +1947,8 @@ fn map_puzzle_board_request_record(board: PuzzleBoardSnapshotResponse) -> Puzzle
|
||||
fn map_puzzle_runtime_level_response(
|
||||
level: spacetime_client::PuzzleRuntimeLevelRecord,
|
||||
) -> PuzzleRuntimeLevelSnapshotResponse {
|
||||
let timer_defaults = build_puzzle_runtime_timer_response_defaults(level.grid_size);
|
||||
let timer_defaults =
|
||||
build_puzzle_runtime_timer_response_defaults(level.level_index, level.grid_size);
|
||||
let time_limit_ms = if level.time_limit_ms == 0 {
|
||||
timer_defaults.time_limit_ms
|
||||
} else {
|
||||
@@ -1945,9 +1995,14 @@ struct PuzzleRuntimeTimerResponseDefaults {
|
||||
}
|
||||
|
||||
fn build_puzzle_runtime_timer_response_defaults(
|
||||
level_index: u32,
|
||||
grid_size: u32,
|
||||
) -> PuzzleRuntimeTimerResponseDefaults {
|
||||
let time_limit_ms = module_puzzle::resolve_puzzle_level_time_limit_ms(grid_size);
|
||||
let time_limit_ms = if level_index > 0 {
|
||||
module_puzzle::resolve_puzzle_level_time_limit_ms_by_index(level_index)
|
||||
} else {
|
||||
module_puzzle::resolve_puzzle_level_time_limit_ms(grid_size)
|
||||
};
|
||||
PuzzleRuntimeTimerResponseDefaults { time_limit_ms }
|
||||
}
|
||||
|
||||
@@ -2697,8 +2752,11 @@ async fn build_local_next_puzzle_run(
|
||||
return Ok(next_run);
|
||||
}
|
||||
|
||||
if let Some(gallery_item) = resolve_gallery_next_puzzle_work(state, &run).await? {
|
||||
return Ok(build_next_run_from_puzzle_work(state, run, gallery_item));
|
||||
let current_work = fetch_local_current_work_detail(state, &run).await?;
|
||||
let similar_works =
|
||||
resolve_gallery_similar_puzzle_works(state, &run, current_work.as_ref()).await?;
|
||||
if !similar_works.is_empty() {
|
||||
return Ok(build_local_similar_works_handoff(run, similar_works));
|
||||
}
|
||||
|
||||
if source_session_id.trim().is_empty() {
|
||||
@@ -2886,23 +2944,187 @@ async fn fetch_local_current_work_detail(
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_gallery_next_puzzle_work(
|
||||
async fn resolve_gallery_similar_puzzle_works(
|
||||
state: &AppState,
|
||||
run: &PuzzleRunRecord,
|
||||
) -> Result<Option<PuzzleWorkProfileRecord>, AppError> {
|
||||
current_work: Option<&PuzzleWorkProfileRecord>,
|
||||
) -> Result<Vec<PuzzleRecommendedNextWorkRecord>, AppError> {
|
||||
let Some(current_profile) = build_recommendation_current_profile(run, current_work) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let items = state
|
||||
.spacetime_client()
|
||||
.list_puzzle_gallery()
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)?;
|
||||
Ok(items.into_iter().find(|item| {
|
||||
item.publication_status == "published"
|
||||
&& item
|
||||
.cover_image_src
|
||||
.as_ref()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
&& !run.played_profile_ids.contains(&item.profile_id)
|
||||
}))
|
||||
let candidates = items
|
||||
.iter()
|
||||
.map(map_puzzle_work_profile_domain)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(module_puzzle::select_next_profiles(
|
||||
¤t_profile,
|
||||
&run.played_profile_ids,
|
||||
&candidates,
|
||||
3,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|candidate| build_recommended_next_work_record(¤t_profile, candidate))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn build_local_similar_works_handoff(
|
||||
mut run: PuzzleRunRecord,
|
||||
recommended_next_works: Vec<PuzzleRecommendedNextWorkRecord>,
|
||||
) -> PuzzleRunRecord {
|
||||
let next_profile_id = recommended_next_works
|
||||
.first()
|
||||
.map(|item| item.profile_id.clone());
|
||||
run.recommended_next_profile_id = next_profile_id.clone();
|
||||
run.next_level_mode = module_puzzle::PUZZLE_NEXT_LEVEL_MODE_SIMILAR_WORKS.to_string();
|
||||
run.next_level_profile_id = next_profile_id;
|
||||
run.next_level_id = None;
|
||||
run.recommended_next_works = recommended_next_works;
|
||||
run
|
||||
}
|
||||
|
||||
fn build_recommendation_current_profile(
|
||||
run: &PuzzleRunRecord,
|
||||
current_work: Option<&PuzzleWorkProfileRecord>,
|
||||
) -> Option<PuzzleWorkProfile> {
|
||||
if let Some(work) = current_work {
|
||||
return Some(map_puzzle_work_profile_domain(work));
|
||||
}
|
||||
|
||||
let level = run.current_level.as_ref()?;
|
||||
Some(PuzzleWorkProfile {
|
||||
work_id: format!("runtime-work-{}", level.profile_id),
|
||||
profile_id: level.profile_id.clone(),
|
||||
owner_user_id: String::new(),
|
||||
source_session_id: None,
|
||||
author_display_name: level.author_display_name.clone(),
|
||||
work_title: level.level_name.clone(),
|
||||
work_description: String::new(),
|
||||
level_name: level.level_name.clone(),
|
||||
summary: String::new(),
|
||||
theme_tags: level.theme_tags.clone(),
|
||||
cover_image_src: level.cover_image_src.clone(),
|
||||
cover_asset_id: None,
|
||||
levels: Vec::new(),
|
||||
publication_status: module_puzzle::PuzzlePublicationStatus::Published,
|
||||
updated_at_micros: 0,
|
||||
published_at_micros: None,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
recent_play_count_7d: 0,
|
||||
point_incentive_total_half_points: 0,
|
||||
point_incentive_claimed_points: 0,
|
||||
publish_ready: true,
|
||||
anchor_pack: module_puzzle::empty_anchor_pack(),
|
||||
})
|
||||
}
|
||||
|
||||
fn map_puzzle_work_profile_domain(item: &PuzzleWorkProfileRecord) -> PuzzleWorkProfile {
|
||||
PuzzleWorkProfile {
|
||||
work_id: item.work_id.clone(),
|
||||
profile_id: item.profile_id.clone(),
|
||||
owner_user_id: item.owner_user_id.clone(),
|
||||
source_session_id: item.source_session_id.clone(),
|
||||
author_display_name: item.author_display_name.clone(),
|
||||
work_title: item.work_title.clone(),
|
||||
work_description: item.work_description.clone(),
|
||||
level_name: item.level_name.clone(),
|
||||
summary: item.summary.clone(),
|
||||
theme_tags: item.theme_tags.clone(),
|
||||
cover_image_src: item.cover_image_src.clone(),
|
||||
cover_asset_id: item.cover_asset_id.clone(),
|
||||
levels: item
|
||||
.levels
|
||||
.iter()
|
||||
.map(map_puzzle_draft_level_domain)
|
||||
.collect(),
|
||||
publication_status: match item.publication_status.as_str() {
|
||||
"published" => module_puzzle::PuzzlePublicationStatus::Published,
|
||||
_ => module_puzzle::PuzzlePublicationStatus::Draft,
|
||||
},
|
||||
updated_at_micros: parse_puzzle_record_timestamp_micros(&item.updated_at),
|
||||
published_at_micros: item
|
||||
.published_at
|
||||
.as_deref()
|
||||
.map(parse_puzzle_record_timestamp_micros),
|
||||
play_count: item.play_count,
|
||||
remix_count: item.remix_count,
|
||||
like_count: item.like_count,
|
||||
recent_play_count_7d: item.recent_play_count_7d,
|
||||
point_incentive_total_half_points: item.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: item.point_incentive_claimed_points,
|
||||
publish_ready: item.publish_ready,
|
||||
anchor_pack: module_puzzle::empty_anchor_pack(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_puzzle_draft_level_domain(
|
||||
level: &PuzzleDraftLevelRecord,
|
||||
) -> module_puzzle::PuzzleDraftLevel {
|
||||
module_puzzle::PuzzleDraftLevel {
|
||||
level_id: level.level_id.clone(),
|
||||
level_name: level.level_name.clone(),
|
||||
picture_description: level.picture_description.clone(),
|
||||
candidates: level
|
||||
.candidates
|
||||
.iter()
|
||||
.map(map_puzzle_generated_image_candidate_domain)
|
||||
.collect(),
|
||||
selected_candidate_id: level.selected_candidate_id.clone(),
|
||||
cover_image_src: level.cover_image_src.clone(),
|
||||
cover_asset_id: level.cover_asset_id.clone(),
|
||||
generation_status: level.generation_status.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_puzzle_generated_image_candidate_domain(
|
||||
candidate: &PuzzleGeneratedImageCandidateRecord,
|
||||
) -> PuzzleGeneratedImageCandidate {
|
||||
PuzzleGeneratedImageCandidate {
|
||||
candidate_id: candidate.candidate_id.clone(),
|
||||
image_src: candidate.image_src.clone(),
|
||||
asset_id: candidate.asset_id.clone(),
|
||||
prompt: candidate.prompt.clone(),
|
||||
actual_prompt: candidate.actual_prompt.clone(),
|
||||
source_type: candidate.source_type.clone(),
|
||||
selected: candidate.selected,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_recommended_next_work_record(
|
||||
current_profile: &PuzzleWorkProfile,
|
||||
candidate: &PuzzleWorkProfile,
|
||||
) -> PuzzleRecommendedNextWorkRecord {
|
||||
PuzzleRecommendedNextWorkRecord {
|
||||
profile_id: candidate.profile_id.clone(),
|
||||
level_name: candidate.level_name.clone(),
|
||||
author_display_name: candidate.author_display_name.clone(),
|
||||
theme_tags: candidate.theme_tags.clone(),
|
||||
cover_image_src: candidate.cover_image_src.clone(),
|
||||
similarity_score: module_puzzle::tag_similarity_score(
|
||||
¤t_profile.theme_tags,
|
||||
&candidate.theme_tags,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_puzzle_record_timestamp_micros(value: &str) -> i64 {
|
||||
let Some((seconds, rest)) = value.split_once('.') else {
|
||||
return 0;
|
||||
};
|
||||
let micros = rest.strip_suffix('Z').unwrap_or(rest);
|
||||
let Ok(seconds) = seconds.parse::<i64>() else {
|
||||
return 0;
|
||||
};
|
||||
let Ok(micros) = micros.parse::<i64>() else {
|
||||
return 0;
|
||||
};
|
||||
seconds.saturating_mul(1_000_000).saturating_add(micros)
|
||||
}
|
||||
|
||||
fn pick_unused_puzzle_candidate<'a>(
|
||||
@@ -2987,27 +3209,6 @@ fn resolve_level_cover_image_src(level: &PuzzleDraftLevelRecord) -> Option<Strin
|
||||
})
|
||||
}
|
||||
|
||||
fn build_next_run_from_puzzle_work(
|
||||
state: &AppState,
|
||||
run: PuzzleRunRecord,
|
||||
item: PuzzleWorkProfileRecord,
|
||||
) -> PuzzleRunRecord {
|
||||
let author = resolve_work_author_by_user_id(
|
||||
state,
|
||||
&item.owner_user_id,
|
||||
Some(&item.author_display_name),
|
||||
None,
|
||||
);
|
||||
build_next_run_from_parts(
|
||||
run,
|
||||
item.profile_id,
|
||||
item.level_name,
|
||||
author.display_name,
|
||||
item.theme_tags,
|
||||
item.cover_image_src,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_next_run_from_candidate(
|
||||
run: PuzzleRunRecord,
|
||||
session: &PuzzleAgentSessionRecord,
|
||||
@@ -3089,8 +3290,9 @@ fn build_next_run_from_parts_with_handoff(
|
||||
next_after_level_id: Option<String>,
|
||||
) -> PuzzleRunRecord {
|
||||
let next_level_index = run.current_level_index + 1;
|
||||
let grid_size = if run.cleared_level_count >= 3 { 4 } else { 3 };
|
||||
let time_limit_ms = module_puzzle::resolve_puzzle_level_time_limit_ms(grid_size);
|
||||
let level_config = resolve_puzzle_level_config(next_level_index);
|
||||
let grid_size = level_config.grid_size;
|
||||
let time_limit_ms = level_config.time_limit_ms;
|
||||
let mut played_profile_ids = run.played_profile_ids.clone();
|
||||
let current_level_id = run.next_level_id.clone();
|
||||
if !played_profile_ids.contains(&profile_id) {
|
||||
@@ -3250,6 +3452,98 @@ mod tests {
|
||||
assert!(!has_original_neighbor_pair(&third));
|
||||
}
|
||||
|
||||
fn test_recommended_work(profile_id: &str, score: f32) -> PuzzleRecommendedNextWorkRecord {
|
||||
PuzzleRecommendedNextWorkRecord {
|
||||
profile_id: profile_id.to_string(),
|
||||
level_name: format!("{profile_id} 关"),
|
||||
author_display_name: "作者".to_string(),
|
||||
theme_tags: vec!["奇幻".to_string()],
|
||||
cover_image_src: Some(format!("/{profile_id}.png")),
|
||||
similarity_score: score,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_similar_works_handoff_keeps_cleared_run_for_user_choice() {
|
||||
let run = PuzzleRunRecord {
|
||||
run_id: "local-puzzle-run-a".to_string(),
|
||||
entry_profile_id: "profile-current".to_string(),
|
||||
cleared_level_count: 1,
|
||||
current_level_index: 1,
|
||||
current_grid_size: 3,
|
||||
played_profile_ids: vec!["profile-current".to_string()],
|
||||
previous_level_tags: vec!["奇幻".to_string()],
|
||||
current_level: Some(PuzzleRuntimeLevelRecord {
|
||||
run_id: "local-puzzle-run-a".to_string(),
|
||||
level_index: 1,
|
||||
level_id: Some("puzzle-level-1".to_string()),
|
||||
grid_size: 3,
|
||||
profile_id: "profile-current".to_string(),
|
||||
level_name: "当前拼图".to_string(),
|
||||
author_display_name: "当前作者".to_string(),
|
||||
theme_tags: vec!["奇幻".to_string()],
|
||||
cover_image_src: Some("/current.png".to_string()),
|
||||
board: build_local_puzzle_board(3, "local-puzzle-run-a", "profile-current", 1),
|
||||
status: "cleared".to_string(),
|
||||
started_at_ms: 1_000,
|
||||
cleared_at_ms: Some(2_000),
|
||||
elapsed_ms: Some(1_000),
|
||||
time_limit_ms: 300_000,
|
||||
remaining_ms: 0,
|
||||
paused_accumulated_ms: 0,
|
||||
pause_started_at_ms: None,
|
||||
freeze_accumulated_ms: 0,
|
||||
freeze_started_at_ms: None,
|
||||
freeze_until_ms: None,
|
||||
leaderboard_entries: Vec::new(),
|
||||
}),
|
||||
recommended_next_profile_id: None,
|
||||
next_level_mode: module_puzzle::PUZZLE_NEXT_LEVEL_MODE_NONE.to_string(),
|
||||
next_level_profile_id: None,
|
||||
next_level_id: None,
|
||||
recommended_next_works: Vec::new(),
|
||||
leaderboard_entries: Vec::new(),
|
||||
};
|
||||
|
||||
let next_run = build_local_similar_works_handoff(
|
||||
run,
|
||||
vec![
|
||||
test_recommended_work("profile-a", 0.9),
|
||||
test_recommended_work("profile-b", 0.8),
|
||||
test_recommended_work("profile-c", 0.7),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
next_run.next_level_mode,
|
||||
module_puzzle::PUZZLE_NEXT_LEVEL_MODE_SIMILAR_WORKS
|
||||
);
|
||||
assert_eq!(
|
||||
next_run.recommended_next_profile_id.as_deref(),
|
||||
Some("profile-a")
|
||||
);
|
||||
assert_eq!(next_run.next_level_profile_id.as_deref(), Some("profile-a"));
|
||||
assert_eq!(next_run.next_level_id, None);
|
||||
assert_eq!(next_run.recommended_next_works.len(), 3);
|
||||
assert_eq!(next_run.current_level_index, 1);
|
||||
assert_eq!(
|
||||
next_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| level.status.as_str()),
|
||||
Some("cleared")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_record_timestamp_parser_matches_shared_format() {
|
||||
assert_eq!(
|
||||
parse_puzzle_record_timestamp_micros("1713686401.234567Z"),
|
||||
1_713_686_401_234_567
|
||||
);
|
||||
assert_eq!(parse_puzzle_record_timestamp_micros("bad-value"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_generated_image_size_is_square_1_1() {
|
||||
assert_eq!(PUZZLE_GENERATED_IMAGE_SIZE, "1024*1024");
|
||||
|
||||
@@ -21,6 +21,7 @@ use shared_contracts::runtime::{
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC, ProfileDashboardSummaryResponse,
|
||||
ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse,
|
||||
@@ -127,6 +128,9 @@ fn format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::RedeemCodeReward => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD
|
||||
}
|
||||
RuntimeProfileWalletLedgerSourceType::PuzzleAuthorIncentiveClaim => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +566,7 @@ mod tests {
|
||||
use crate::{app::build_router, config::AppConfig, state::AppState};
|
||||
|
||||
#[test]
|
||||
fn profile_wallet_ledger_source_type_formats_asset_operation_values() {
|
||||
fn profile_wallet_ledger_source_type_formats_backend_values() {
|
||||
assert_eq!(
|
||||
format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationConsume
|
||||
@@ -575,6 +579,12 @@ mod tests {
|
||||
),
|
||||
shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND
|
||||
);
|
||||
assert_eq!(
|
||||
format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::PuzzleAuthorIncentiveClaim
|
||||
),
|
||||
shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -262,6 +262,7 @@ pub enum RuntimeProfileWalletLedgerSourceType {
|
||||
AssetOperationConsume,
|
||||
AssetOperationRefund,
|
||||
RedeemCodeReward,
|
||||
PuzzleAuthorIncentiveClaim,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
@@ -1709,6 +1710,7 @@ impl RuntimeProfileWalletLedgerSourceType {
|
||||
Self::AssetOperationConsume => "asset_operation_consume",
|
||||
Self::AssetOperationRefund => "asset_operation_refund",
|
||||
Self::RedeemCodeReward => "redeem_code_reward",
|
||||
Self::PuzzleAuthorIncentiveClaim => "puzzle_author_incentive_claim",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2233,6 +2235,10 @@ mod tests {
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationRefund.as_str(),
|
||||
"asset_operation_refund"
|
||||
);
|
||||
assert_eq!(
|
||||
RuntimeProfileWalletLedgerSourceType::PuzzleAuthorIncentiveClaim.as_str(),
|
||||
"puzzle_author_incentive_claim"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -47,11 +47,61 @@ pub struct PuzzleWorkSummaryResponse {
|
||||
pub like_count: u32,
|
||||
#[serde(default)]
|
||||
pub recent_play_count_7d: u32,
|
||||
#[serde(default)]
|
||||
pub point_incentive_total_half_points: u64,
|
||||
#[serde(default)]
|
||||
pub point_incentive_claimed_points: u64,
|
||||
#[serde(default)]
|
||||
pub point_incentive_total_points: f64,
|
||||
#[serde(default)]
|
||||
pub point_incentive_claimable_points: u64,
|
||||
pub publish_ready: bool,
|
||||
#[serde(default)]
|
||||
pub levels: Vec<PuzzleDraftLevelResponse>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn puzzle_work_summary_response_uses_point_incentive_fields() {
|
||||
let payload = serde_json::to_value(PuzzleWorkSummaryResponse {
|
||||
work_id: "work-1".to_string(),
|
||||
profile_id: "profile-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_session_id: None,
|
||||
author_display_name: "作者".to_string(),
|
||||
work_title: "作品".to_string(),
|
||||
work_description: "描述".to_string(),
|
||||
level_name: "第一关".to_string(),
|
||||
summary: "画面".to_string(),
|
||||
theme_tags: vec!["拼图".to_string(), "夜色".to_string(), "灯光".to_string()],
|
||||
cover_image_src: None,
|
||||
cover_asset_id: None,
|
||||
publication_status: "published".to_string(),
|
||||
updated_at: "2026-05-01T00:00:00Z".to_string(),
|
||||
published_at: Some("2026-05-01T00:00:00Z".to_string()),
|
||||
play_count: 1,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
recent_play_count_7d: 1,
|
||||
point_incentive_total_half_points: 3,
|
||||
point_incentive_claimed_points: 1,
|
||||
point_incentive_total_points: 1.5,
|
||||
point_incentive_claimable_points: 0,
|
||||
publish_ready: true,
|
||||
levels: Vec::new(),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["pointIncentiveTotalHalfPoints"], 3);
|
||||
assert_eq!(payload["pointIncentiveClaimedPoints"], 1);
|
||||
assert_eq!(payload["pointIncentiveTotalPoints"], 1.5);
|
||||
assert_eq!(payload["pointIncentiveClaimablePoints"], 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PuzzleWorkProfileResponse {
|
||||
|
||||
@@ -11,6 +11,8 @@ pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME: &str =
|
||||
"asset_operation_consume";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND: &str = "asset_operation_refund";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD: &str = "redeem_code_reward";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM: &str =
|
||||
"puzzle_author_incentive_claim";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina";
|
||||
@@ -910,6 +912,14 @@ mod tests {
|
||||
.to_string(),
|
||||
created_at: "2026-04-22T10:05:00Z".to_string(),
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-7".to_string(),
|
||||
amount_delta: 2,
|
||||
balance_after: 202,
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM
|
||||
.to_string(),
|
||||
created_at: "2026-04-22T10:06:00Z".to_string(),
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
@@ -940,6 +950,10 @@ mod tests {
|
||||
payload["entries"][5]["sourceType"],
|
||||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["entries"][6]["sourceType"],
|
||||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["entries"][0]["createdAt"],
|
||||
json!("2026-04-22T10:00:00Z")
|
||||
|
||||
@@ -39,9 +39,9 @@ pub use mapper::{
|
||||
PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunNextLevelRecordInput,
|
||||
PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord,
|
||||
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord,
|
||||
PuzzleSelectCoverImageRecordInput, PuzzleWorkLikeReportRecordInput, PuzzleWorkProfileRecord,
|
||||
PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput, ResolveCombatActionRecord,
|
||||
ResolveNpcBattleInteractionInput,
|
||||
PuzzleSelectCoverImageRecordInput, PuzzleWorkLikeReportRecordInput,
|
||||
PuzzleWorkPointIncentiveClaimRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput,
|
||||
PuzzleWorkUpsertRecordInput, ResolveCombatActionRecord, ResolveNpcBattleInteractionInput,
|
||||
};
|
||||
|
||||
pub mod ai;
|
||||
|
||||
@@ -2436,6 +2436,8 @@ pub(crate) fn map_puzzle_work_profile(
|
||||
remix_count: snapshot.remix_count,
|
||||
like_count: snapshot.like_count,
|
||||
recent_play_count_7d: snapshot.recent_play_count_7d,
|
||||
point_incentive_total_half_points: snapshot.point_incentive_total_half_points,
|
||||
point_incentive_claimed_points: snapshot.point_incentive_claimed_points,
|
||||
publish_ready: snapshot.publish_ready,
|
||||
anchor_pack: map_puzzle_anchor_pack(snapshot.anchor_pack),
|
||||
levels: snapshot
|
||||
@@ -2491,6 +2493,13 @@ fn map_puzzle_recommended_next_work(
|
||||
pub(crate) fn map_puzzle_runtime_level_snapshot(
|
||||
snapshot: DomainPuzzleRuntimeLevelSnapshot,
|
||||
) -> PuzzleRuntimeLevelRecord {
|
||||
// 中文注释:历史 run_json 可能缺 started_at_ms,领域 serde 会回填为 0;API 层继续补成 1,避免前端计时器拿到无效开局时间。
|
||||
let started_at_ms = if snapshot.started_at_ms == 0 {
|
||||
1
|
||||
} else {
|
||||
snapshot.started_at_ms
|
||||
};
|
||||
|
||||
PuzzleRuntimeLevelRecord {
|
||||
run_id: snapshot.run_id,
|
||||
level_index: snapshot.level_index,
|
||||
@@ -2503,7 +2512,7 @@ pub(crate) fn map_puzzle_runtime_level_snapshot(
|
||||
cover_image_src: snapshot.cover_image_src,
|
||||
board: map_puzzle_board_snapshot(snapshot.board),
|
||||
status: snapshot.status.as_str().to_string(),
|
||||
started_at_ms: snapshot.started_at_ms,
|
||||
started_at_ms,
|
||||
cleared_at_ms: snapshot.cleared_at_ms,
|
||||
elapsed_ms: snapshot.elapsed_ms,
|
||||
time_limit_ms: snapshot.time_limit_ms,
|
||||
@@ -3485,6 +3494,9 @@ pub(crate) fn map_runtime_profile_wallet_ledger_source_type_back(
|
||||
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::RedeemCodeReward => {
|
||||
module_runtime::RuntimeProfileWalletLedgerSourceType::RedeemCodeReward
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::PuzzleAuthorIncentiveClaim => {
|
||||
module_runtime::RuntimeProfileWalletLedgerSourceType::PuzzleAuthorIncentiveClaim
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4535,6 +4547,7 @@ pub struct PuzzleRunPropRecordInput {
|
||||
pub owner_user_id: String,
|
||||
pub prop_kind: String,
|
||||
pub used_at_micros: i64,
|
||||
pub spent_points: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -4716,11 +4729,20 @@ pub struct PuzzleWorkProfileRecord {
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub recent_play_count_7d: u32,
|
||||
pub point_incentive_total_half_points: u64,
|
||||
pub point_incentive_claimed_points: u64,
|
||||
pub publish_ready: bool,
|
||||
pub anchor_pack: PuzzleAnchorPackRecord,
|
||||
pub levels: Vec<PuzzleDraftLevelRecord>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PuzzleWorkPointIncentiveClaimRecordInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub claimed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PuzzleCellPositionRecord {
|
||||
pub row: u32,
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{
|
||||
self as __sdk,
|
||||
__lib,
|
||||
__sats,
|
||||
__ws,
|
||||
};
|
||||
|
||||
use super::puzzle_work_point_incentive_claim_input_type::PuzzleWorkPointIncentiveClaimInput;
|
||||
use super::puzzle_work_procedure_result_type::PuzzleWorkProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct ClaimPuzzleWorkPointIncentiveArgs {
|
||||
pub input: PuzzleWorkPointIncentiveClaimInput,
|
||||
}
|
||||
|
||||
|
||||
impl __sdk::InModule for ClaimPuzzleWorkPointIncentiveArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `claim_puzzle_work_point_incentive`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait claim_puzzle_work_point_incentive {
|
||||
fn claim_puzzle_work_point_incentive(&self, input: PuzzleWorkPointIncentiveClaimInput,
|
||||
) {
|
||||
self.claim_puzzle_work_point_incentive_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn claim_puzzle_work_point_incentive_then(
|
||||
&self,
|
||||
input: PuzzleWorkPointIncentiveClaimInput,
|
||||
|
||||
__callback: impl FnOnce(&super::ProcedureEventContext, Result<PuzzleWorkProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl claim_puzzle_work_point_incentive for super::RemoteProcedures {
|
||||
fn claim_puzzle_work_point_incentive_then(
|
||||
&self,
|
||||
input: PuzzleWorkPointIncentiveClaimInput,
|
||||
|
||||
__callback: impl FnOnce(&super::ProcedureEventContext, Result<PuzzleWorkProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||
) {
|
||||
self.imp.invoke_procedure_with_callback::<_, PuzzleWorkProcedureResult>(
|
||||
"claim_puzzle_work_point_incentive",
|
||||
ClaimPuzzleWorkPointIncentiveArgs { input, },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{
|
||||
self as __sdk,
|
||||
__lib,
|
||||
__sats,
|
||||
__ws,
|
||||
};
|
||||
|
||||
use super::match_3_d_run_click_input_type::Match3DRunClickInput;
|
||||
use super::match_3_d_click_item_procedure_result_type::Match3DClickItemProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct ClickMatch3DItemArgs {
|
||||
pub input: Match3DRunClickInput,
|
||||
}
|
||||
|
||||
|
||||
impl __sdk::InModule for ClickMatch3DItemArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `click_match_3_d_item`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait click_match_3_d_item {
|
||||
fn click_match_3_d_item(&self, input: Match3DRunClickInput,
|
||||
) {
|
||||
self.click_match_3_d_item_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn click_match_3_d_item_then(
|
||||
&self,
|
||||
input: Match3DRunClickInput,
|
||||
|
||||
__callback: impl FnOnce(&super::ProcedureEventContext, Result<Match3DClickItemProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl click_match_3_d_item for super::RemoteProcedures {
|
||||
fn click_match_3_d_item_then(
|
||||
&self,
|
||||
input: Match3DRunClickInput,
|
||||
|
||||
__callback: impl FnOnce(&super::ProcedureEventContext, Result<Match3DClickItemProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||
) {
|
||||
self.imp.invoke_procedure_with_callback::<_, Match3DClickItemProcedureResult>(
|
||||
"click_match_3_d_item",
|
||||
ClickMatch3DItemArgs { input, },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{
|
||||
self as __sdk,
|
||||
__lib,
|
||||
__sats,
|
||||
__ws,
|
||||
};
|
||||
|
||||
use super::match_3_d_draft_compile_input_type::Match3DDraftCompileInput;
|
||||
use super::match_3_d_agent_session_procedure_result_type::Match3DAgentSessionProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct CompileMatch3DDraftArgs {
|
||||
pub input: Match3DDraftCompileInput,
|
||||
}
|
||||
|
||||
|
||||
impl __sdk::InModule for CompileMatch3DDraftArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `compile_match_3_d_draft`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait compile_match_3_d_draft {
|
||||
fn compile_match_3_d_draft(&self, input: Match3DDraftCompileInput,
|
||||
) {
|
||||
self.compile_match_3_d_draft_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn compile_match_3_d_draft_then(
|
||||
&self,
|
||||
input: Match3DDraftCompileInput,
|
||||
|
||||
__callback: impl FnOnce(&super::ProcedureEventContext, Result<Match3DAgentSessionProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl compile_match_3_d_draft for super::RemoteProcedures {
|
||||
fn compile_match_3_d_draft_then(
|
||||
&self,
|
||||
input: Match3DDraftCompileInput,
|
||||
|
||||
__callback: impl FnOnce(&super::ProcedureEventContext, Result<Match3DAgentSessionProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||
) {
|
||||
self.imp.invoke_procedure_with_callback::<_, Match3DAgentSessionProcedureResult>(
|
||||
"compile_match_3_d_draft",
|
||||
CompileMatch3DDraftArgs { input, },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{
|
||||
self as __sdk,
|
||||
__lib,
|
||||
__sats,
|
||||
__ws,
|
||||
};
|
||||
|
||||
use super::match_3_d_agent_session_procedure_result_type::Match3DAgentSessionProcedureResult;
|
||||
use super::match_3_d_agent_session_create_input_type::Match3DAgentSessionCreateInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct CreateMatch3DAgentSessionArgs {
|
||||
pub input: Match3DAgentSessionCreateInput,
|
||||
}
|
||||
|
||||
|
||||
impl __sdk::InModule for CreateMatch3DAgentSessionArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `create_match_3_d_agent_session`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait create_match_3_d_agent_session {
|
||||
fn create_match_3_d_agent_session(&self, input: Match3DAgentSessionCreateInput,
|
||||
) {
|
||||
self.create_match_3_d_agent_session_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn create_match_3_d_agent_session_then(
|
||||
&self,
|
||||
input: Match3DAgentSessionCreateInput,
|
||||
|
||||
__callback: impl FnOnce(&super::ProcedureEventContext, Result<Match3DAgentSessionProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl create_match_3_d_agent_session for super::RemoteProcedures {
|
||||
fn create_match_3_d_agent_session_then(
|
||||
&self,
|
||||
input: Match3DAgentSessionCreateInput,
|
||||
|
||||
__callback: impl FnOnce(&super::ProcedureEventContext, Result<Match3DAgentSessionProcedureResult, __sdk::InternalError>) + Send + 'static,
|
||||
) {
|
||||
self.imp.invoke_procedure_with_callback::<_, Match3DAgentSessionProcedureResult>(
|
||||
"create_match_3_d_agent_session",
|
||||
CreateMatch3DAgentSessionArgs { input, },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user