From 2e9d0f46407898044fe1f47e80dff888fa08c2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Fri, 1 May 2026 01:30:02 +0800 Subject: [PATCH] 1 --- ...ATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md | 42 +- .../MY_TAB_DATA_DASHBOARD_PRD_2026-04-16.md | 5 +- ...AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md | 4 +- ...VEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md | 9 +- ...ZZLE_RUNTIME_TIMER_AND_PROPS_2026-04-29.md | 24 +- .../PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md | 59 +++ docs/technical/README.md | 3 +- .../src/contracts/puzzleRuntimeSession.ts | 2 +- .../shared/src/contracts/puzzleWorkSummary.ts | 4 + packages/shared/src/contracts/runtime.ts | 3 +- ...24-000001-parse_response_failed.input.json | 18 + ...24-000001-parse_response_failed.output.txt | 1 + server-rs/crates/api-server/src/app.rs | 21 +- server-rs/crates/api-server/src/puzzle.rs | 374 +++++++++++++-- .../crates/api-server/src/runtime_profile.rs | 12 +- server-rs/crates/module-puzzle/src/lib.rs | 437 +++++++++++++++++- server-rs/crates/module-runtime/src/lib.rs | 6 + .../shared-contracts/src/puzzle_works.rs | 50 ++ .../crates/shared-contracts/src/runtime.rs | 14 + server-rs/crates/spacetime-client/src/lib.rs | 6 +- .../crates/spacetime-client/src/mapper.rs | 24 +- ...m_puzzle_work_point_incentive_procedure.rs | 58 +++ .../click_match_3_d_item_procedure.rs | 58 +++ .../compile_match_3_d_draft_procedure.rs | 58 +++ ...reate_match_3_d_agent_session_procedure.rs | 58 +++ .../delete_match_3_d_work_procedure.rs | 58 +++ ..._match_3_d_agent_message_turn_procedure.rs | 58 +++ .../finish_match_3_d_time_up_procedure.rs | 58 +++ .../get_match_3_d_agent_session_procedure.rs | 58 +++ .../get_match_3_d_run_procedure.rs | 58 +++ .../get_match_3_d_work_detail_procedure.rs | 58 +++ .../get_puzzle_gallery_detail_procedure.rs | 2 +- .../get_puzzle_work_detail_procedure.rs | 2 +- .../list_match_3_d_works_procedure.rs | 58 +++ ...h_3_d_agent_message_finalize_input_type.rs | 31 ++ .../match_3_d_agent_message_row_type.rs | 77 +++ ...tch_3_d_agent_message_submit_input_type.rs | 27 ++ ...tch_3_d_agent_session_create_input_type.rs | 29 ++ .../match_3_d_agent_session_get_input_type.rs | 24 + ...3_d_agent_session_procedure_result_type.rs | 25 + .../match_3_d_agent_session_row_type.rs | 95 ++++ ...ch_3_d_click_item_procedure_result_type.rs | 29 ++ .../match_3_d_draft_compile_input_type.rs | 32 ++ .../match_3_d_run_click_input_type.rs | 28 ++ .../match_3_d_run_get_input_type.rs | 24 + .../match_3_d_run_procedure_result_type.rs | 25 + .../match_3_d_run_restart_input_type.rs | 26 ++ .../match_3_d_run_start_input_type.rs | 26 ++ .../match_3_d_run_stop_input_type.rs | 25 + .../match_3_d_run_time_up_input_type.rs | 25 + .../match_3_d_runtime_run_row_type.rs | 109 +++++ .../match_3_d_work_delete_input_type.rs | 24 + .../match_3_d_work_get_input_type.rs | 24 + .../match_3_d_work_procedure_result_type.rs | 25 + .../match_3_d_work_profile_row_type.rs | 112 +++++ .../match_3_d_work_publish_input_type.rs | 25 + .../match_3_d_work_update_input_type.rs | 33 ++ .../match_3_d_works_list_input_type.rs | 24 + .../match_3_d_works_procedure_result_type.rs | 25 + .../src/module_bindings/mod.rs | 86 ++++ .../publish_match_3_d_work_procedure.rs | 58 +++ .../puzzle_run_prop_input_type.rs | 1 + ...e_work_point_incentive_claim_input_type.rs | 25 + .../puzzle_work_profile_row_type.rs | 6 + .../restart_match_3_d_run_procedure.rs | 58 +++ ..._profile_wallet_ledger_source_type_type.rs | 2 + .../start_match_3_d_run_procedure.rs | 58 +++ .../stop_match_3_d_run_procedure.rs | 58 +++ ...ubmit_match_3_d_agent_message_procedure.rs | 58 +++ .../update_match_3_d_work_procedure.rs | 58 +++ .../crates/spacetime-client/src/puzzle.rs | 25 + .../crates/spacetime-module/src/migration.rs | 6 + .../crates/spacetime-module/src/puzzle.rs | 412 +++++++++++++++-- .../spacetime-module/src/runtime/profile.rs | 18 + ...ustomWorldCreationHub.interaction.test.tsx | 53 +++ .../CustomWorldCreationHub.tsx | 20 + .../custom-world-home/CustomWorldWorkCard.tsx | 101 +++- .../custom-world-home/creationWorkShelf.ts | 40 ++ .../PlatformEntryFlowShellImpl.tsx | 186 +++++++- .../puzzle-result/PuzzleResultView.test.tsx | 1 + .../puzzle-result/PuzzleResultView.tsx | 3 + .../PuzzleRuntimeShell.test.tsx | 132 ++++++ .../puzzle-runtime/PuzzleRuntimeShell.tsx | 2 +- ...gEntryFlowShell.agent.interaction.test.tsx | 1 + .../RpgEntryHomeView.recharge.test.tsx | 12 + src/components/rpg-entry/RpgEntryHomeView.tsx | 2 +- .../rpg-entry/useRpgEntryBootstrap.ts | 20 + src/index.css | 93 ++++ .../puzzle-runtime/puzzleLocalRuntime.test.ts | 158 +++++++ .../puzzle-runtime/puzzleLocalRuntime.ts | 314 +++++++++++-- src/services/puzzle-works/index.ts | 3 +- .../puzzle-works/puzzleWorksClient.ts | 17 + 92 files changed, 4548 insertions(+), 248 deletions(-) create mode 100644 docs/technical/PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md create mode 100644 server-rs/crates/api-server/logs/llm-raw/1777569756314-63624-000001-parse_response_failed.input.json create mode 100644 server-rs/crates/api-server/logs/llm-raw/1777569756314-63624-000001-parse_response_failed.output.txt create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_work_point_incentive_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/click_match_3_d_item_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/compile_match_3_d_draft_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/create_match_3_d_agent_session_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/delete_match_3_d_work_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/finalize_match_3_d_agent_message_turn_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/finish_match_3_d_time_up_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_agent_session_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_work_detail_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/list_match_3_d_works_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_finalize_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_submit_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_create_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_click_item_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_draft_compile_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_click_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_restart_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_start_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_stop_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_time_up_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_runtime_run_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_delete_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_profile_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_publish_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_update_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_list_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/publish_match_3_d_work_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_point_incentive_claim_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/restart_match_3_d_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/start_match_3_d_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/stop_match_3_d_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/submit_match_3_d_agent_message_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/update_match_3_d_work_procedure.rs diff --git a/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md b/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md index a7c05b17..a6e69c01 100644 --- a/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md +++ b/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md @@ -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. 合并块可以整体拖动。 diff --git a/docs/prd/MY_TAB_DATA_DASHBOARD_PRD_2026-04-16.md b/docs/prd/MY_TAB_DATA_DASHBOARD_PRD_2026-04-16.md index 9122a871..caddc4e2 100644 --- a/docs/prd/MY_TAB_DATA_DASHBOARD_PRD_2026-04-16.md +++ b/docs/prd/MY_TAB_DATA_DASHBOARD_PRD_2026-04-16.md @@ -93,8 +93,9 @@ 1. 数字过大时做单位缩略展示 2. “游戏时长”卡固定以小时为单位展示,短时长不切换成分钟,长时长不切换成天 -3. 进入页面先展示骨架屏 -4. 数据请求失败时展示降级文案,不展示假数字 +3. “玩过”卡展示值始终带 `个` 单位,例如 `0个`、`1个`、`1.2万个` +4. 进入页面先展示骨架屏 +5. 数据请求失败时展示降级文案,不展示假数字 --- diff --git a/docs/technical/PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md b/docs/technical/PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md index 82e94152..4f9846c2 100644 --- a/docs/technical/PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md +++ b/docs/technical/PUZZLE_IMAGE_AND_FRONTEND_RULES_ALIGNMENT_2026-04-29.md @@ -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. 移动端运行时棋盘为正方形,并尽量贴近屏幕两侧边缘。 diff --git a/docs/technical/PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md b/docs/technical/PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md index 98091234..8cf7e366 100644 --- a/docs/technical/PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md +++ b/docs/technical/PUZZLE_NEXT_LEVEL_AND_SIMILAR_WORK_HANDOFF_2026-04-30.md @@ -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 拼图模块测试和编码检查通过。 diff --git a/docs/technical/PUZZLE_RUNTIME_TIMER_AND_PROPS_2026-04-29.md b/docs/technical/PUZZLE_RUNTIME_TIMER_AND_PROPS_2026-04-29.md index 5b8d06fc..acc5e584 100644 --- a/docs/technical/PUZZLE_RUNTIME_TIMER_AND_PROPS_2026-04-29.md +++ b/docs/technical/PUZZLE_RUNTIME_TIMER_AND_PROPS_2026-04-29.md @@ -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 里写死另一套时间。 ## 计时规则 diff --git a/docs/technical/PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md b/docs/technical/PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md new file mode 100644 index 00000000..f22b6a1b --- /dev/null +++ b/docs/technical/PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md @@ -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 检查和必要前端测试。 diff --git a/docs/technical/README.md b/docs/technical/README.md index 1cda3785..d7310628 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -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` 仅作为兼容回退。 diff --git a/packages/shared/src/contracts/puzzleRuntimeSession.ts b/packages/shared/src/contracts/puzzleRuntimeSession.ts index a240b3b8..1d195172 100644 --- a/packages/shared/src/contracts/puzzleRuntimeSession.ts +++ b/packages/shared/src/contracts/puzzleRuntimeSession.ts @@ -1,4 +1,4 @@ -export type PuzzleGridSize = 3 | 4; +export type PuzzleGridSize = 3 | 4 | 5 | 6 | 7; export interface PuzzleCellPosition { row: number; diff --git a/packages/shared/src/contracts/puzzleWorkSummary.ts b/packages/shared/src/contracts/puzzleWorkSummary.ts index 0fbeb7c8..3bfd4a44 100644 --- a/packages/shared/src/contracts/puzzleWorkSummary.ts +++ b/packages/shared/src/contracts/puzzleWorkSummary.ts @@ -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[]; } diff --git a/packages/shared/src/contracts/runtime.ts b/packages/shared/src/contracts/runtime.ts index cc18ec9c..447999ff 100644 --- a/packages/shared/src/contracts/runtime.ts +++ b/packages/shared/src/contracts/runtime.ts @@ -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; }; diff --git a/server-rs/crates/api-server/logs/llm-raw/1777569756314-63624-000001-parse_response_failed.input.json b/server-rs/crates/api-server/logs/llm-raw/1777569756314-63624-000001-parse_response_failed.input.json new file mode 100644 index 00000000..a8e4fd2b --- /dev/null +++ b/server-rs/crates/api-server/logs/llm-raw/1777569756314-63624-000001-parse_response_failed.input.json @@ -0,0 +1,18 @@ +{ + "provider": "ark", + "protocol": "responses", + "model": "deepseek-v3-2-251201", + "stream": false, + "attempt": 1, + "maxTokens": null, + "messages": [ + { + "role": "system", + "content": "你是严格的世界草稿 JSON 生成器。\n只输出一个 JSON 对象,不要输出 Markdown、代码块、解释或额外文字。" + }, + { + "role": "user", + "content": "请先根据下面的玩家设定创建一份“世界核心骨架”,后续我会分步骤生成角色名单、场景名单和详细档案。\n你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。\n这一步只保留世界顶层信息与一个开局归处占位,不要输出 playableNpcs、storyNpcs、landmarks,也不要展开人物、地图细节或多幕场景内容。\n玩家设定:\n世界承诺:{\"hook\":\"在失真的海图上追查一场被篡改的沉船事故。\"}\n玩家切入口:{\"entryMotivation\":\"查清父亲沉船真相\",\"openingIdentity\":\"被停职返乡的守灯人\",\"openingProblem\":\"灯塔记录被人改写\"}\n\n输出 JSON 模板:\n{\n \"name\": \"世界名称\",\n \"subtitle\": \"世界副标题\",\n \"summary\": \"世界概述\",\n \"tone\": \"世界基调\",\n \"playerGoal\": \"玩家核心目标\",\n \"templateWorldType\": \"WUXIA|XIANXIA\",\n \"majorFactions\": [\"势力甲\", \"势力乙\"],\n \"coreConflicts\": [\"冲突甲\", \"冲突乙\"],\n \"attributeSchema\": {\n \"slots\": [\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" }\n ]\n },\n \"camp\": {\n \"name\": \"开局归处名称\",\n \"description\": \"这是玩家进入世界后的第一处落脚点描述\"\n }\n}\n\n要求:\n- 所有生成文本都必须使用中文。\n- 这一步只输出顶层 10 个字段:name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts、attributeSchema、camp。\n- 这是一个完全独立的自定义世界;不要在任何正文里直接写出“武侠世界”“仙侠世界”等现成世界名。\n- templateWorldType 只是系统兼容字段,不代表正文应当引用的世界名称。\n- camp 只表示玩家开局时的落脚处占位,更接近归舍、住处、栖居、前哨居所这类“家/归处”的概念;不要在这一步生成开局场景任务、三幕事件或三幕背景。\n- 不要输出 playableNpcs、storyNpcs、landmarks、items,也不要输出任何角色和地图细节。\n- majorFactions 保持 2 到 3 个,coreConflicts 保持 2 到 3 个。\n- attributeSchema 必须是本世界专属的角色六维名称体系,slots 必须恰好 6 个,每个 slot 只输出 name,维度名必须是 2 到 4 个汉字且互不重复。\n- attributeSchema.slots 的 name 禁止使用:生命、法力、护甲、攻击、防御、力量、敏捷、智力、精神;不要写通用 DND 或传统四维属性。\n- 不要在 attributeSchema.slots 内输出 definition、positiveSignals、negativeSignals、combatUseText、socialUseText、explorationUseText 或其他说明字段。\n- 世界设定必须直接源自玩家输入,不要脱离主题乱扩写。\n- 每个字符串尽量简洁:subtitle 控制在 8 到 18 个汉字内,summary 控制在 16 到 32 个汉字内,tone 控制在 6 到 16 个汉字内,playerGoal 控制在 16 到 32 个汉字内,camp.description 控制在 18 到 40 个汉字内。\n- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。" + } + ] +} \ No newline at end of file diff --git a/server-rs/crates/api-server/logs/llm-raw/1777569756314-63624-000001-parse_response_failed.output.txt b/server-rs/crates/api-server/logs/llm-raw/1777569756314-63624-000001-parse_response_failed.output.txt new file mode 100644 index 00000000..0681f0ad --- /dev/null +++ b/server-rs/crates/api-server/logs/llm-raw/1777569756314-63624-000001-parse_response_failed.output.txt @@ -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"} \ No newline at end of file diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 366a5d74..c62a37d7 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -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}", diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index 32588b50..a1c26f29 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -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, + AxumPath(profile_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, 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, Extension(request_context): Extension, @@ -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, AppError> { + current_work: Option<&PuzzleWorkProfileRecord>, +) -> Result, 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::>(); + 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, +) -> 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 { + 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::() else { + return 0; + }; + let Ok(micros) = micros.parse::() 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 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, ) -> 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"); diff --git a/server-rs/crates/api-server/src/runtime_profile.rs b/server-rs/crates/api-server/src/runtime_profile.rs index c9cc5c7c..a41abdae 100644 --- a/server-rs/crates/api-server/src/runtime_profile.rs +++ b/server-rs/crates/api-server/src/runtime_profile.rs @@ -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] diff --git a/server-rs/crates/module-puzzle/src/lib.rs b/server-rs/crates/module-puzzle/src/lib.rs index 73b73168..e4f0d620 100644 --- a/server-rs/crates/module-puzzle/src/lib.rs +++ b/server-rs/crates/module-puzzle/src/lib.rs @@ -20,8 +20,16 @@ pub const PUZZLE_EXTEND_TIME_DURATION_MS: u64 = 60_000; pub const PUZZLE_NEXT_LEVEL_MODE_SAME_WORK: &str = "sameWork"; pub const PUZZLE_NEXT_LEVEL_MODE_SIMILAR_WORKS: &str = "similarWorks"; pub const PUZZLE_NEXT_LEVEL_MODE_NONE: &str = "none"; +pub const PUZZLE_SUPPORTED_GRID_SIZES: [u32; 5] = [3, 4, 5, 6, 7]; const PUZZLE_INITIAL_SHUFFLE_ATTEMPTS: u64 = 64; +// 中文注释:拼图难度只从关卡序号解析,避免切割规格和倒计时在不同入口各写一套。 +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PuzzleLevelConfig { + pub grid_size: u32, + pub time_limit_ms: u64, +} + #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum PuzzleAgentStage { @@ -257,6 +265,10 @@ pub struct PuzzleWorkProfile { 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, pub publish_ready: bool, pub anchor_pack: PuzzleAnchorPack, } @@ -540,6 +552,14 @@ pub struct PuzzleWorkLikeRecordInput { pub liked_at_micros: i64, } +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleWorkPointIncentiveClaimInput { + pub profile_id: String, + pub owner_user_id: String, + pub claimed_at_micros: i64, +} + #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PuzzleRunStartInput { @@ -602,6 +622,8 @@ pub struct PuzzleRunPropInput { pub owner_user_id: String, pub prop_kind: String, pub used_at_micros: i64, + #[serde(default)] + pub spent_points: u64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] @@ -1298,6 +1320,8 @@ pub fn create_work_profile( remix_count: 0, like_count: 0, recent_play_count_7d: 0, + point_incentive_total_half_points: 0, + point_incentive_claimed_points: 0, publish_ready: preview.publish_ready, anchor_pack: draft.anchor_pack.clone(), }) @@ -1411,20 +1435,81 @@ pub fn normalize_puzzle_levels( Ok(normalized_levels) } +pub fn is_supported_puzzle_grid_size(grid_size: u32) -> bool { + PUZZLE_SUPPORTED_GRID_SIZES.contains(&grid_size) +} + +pub fn resolve_puzzle_level_config(level_index: u32) -> PuzzleLevelConfig { + let level_index = level_index.max(1); + match level_index { + 1 => PuzzleLevelConfig { + grid_size: 3, + time_limit_ms: 300_000, + }, + 2 => PuzzleLevelConfig { + grid_size: 4, + time_limit_ms: 300_000, + }, + 3 => PuzzleLevelConfig { + grid_size: 5, + time_limit_ms: 300_000, + }, + 4 => PuzzleLevelConfig { + grid_size: 5, + time_limit_ms: 210_000, + }, + _ => { + let loop_index = (level_index.saturating_sub(5) % 6) + 5; + match loop_index { + 5 => PuzzleLevelConfig { + grid_size: 5, + time_limit_ms: 210_000, + }, + 6 => PuzzleLevelConfig { + grid_size: 6, + time_limit_ms: 240_000, + }, + 7 => PuzzleLevelConfig { + grid_size: 5, + time_limit_ms: 210_000, + }, + 8 => PuzzleLevelConfig { + grid_size: 7, + time_limit_ms: 270_000, + }, + 9 => PuzzleLevelConfig { + grid_size: 5, + time_limit_ms: 240_000, + }, + _ => PuzzleLevelConfig { + grid_size: 7, + time_limit_ms: 270_000, + }, + } + } + } +} + pub fn resolve_puzzle_grid_size(cleared_level_count: u32) -> u32 { - if cleared_level_count >= 3 { 4 } else { 3 } + resolve_puzzle_level_config(cleared_level_count + 1).grid_size +} + +pub fn resolve_puzzle_level_time_limit_ms_by_index(level_index: u32) -> u64 { + resolve_puzzle_level_config(level_index.max(1)).time_limit_ms } pub fn resolve_puzzle_level_time_limit_ms(grid_size: u32) -> u64 { match grid_size { - 4 => 300_000, - _ => 180_000, + 3 | 4 | 5 => 300_000, + 6 => 240_000, + 7 => 270_000, + _ => 300_000, } } pub fn resolve_puzzle_runtime_remaining_ms(level: &PuzzleRuntimeLevelSnapshot, now_ms: u64) -> u64 { let time_limit_ms = if level.time_limit_ms == 0 { - resolve_puzzle_level_time_limit_ms(level.grid_size) + resolve_puzzle_level_time_limit_ms_by_index(level.level_index) } else { level.time_limit_ms }; @@ -1436,7 +1521,7 @@ fn normalize_timer_fields(level: &mut PuzzleRuntimeLevelSnapshot, now_ms: u64) { level.started_at_ms = now_ms; } if level.time_limit_ms == 0 { - level.time_limit_ms = resolve_puzzle_level_time_limit_ms(level.grid_size); + level.time_limit_ms = resolve_puzzle_level_time_limit_ms_by_index(level.level_index); } if level.remaining_ms == 0 && level.status == PuzzleRuntimeLevelStatus::Playing { level.remaining_ms = level.time_limit_ms; @@ -1612,7 +1697,7 @@ pub fn build_initial_board_with_seed( grid_size: u32, shuffle_seed: u64, ) -> Result { - if !matches!(grid_size, 3 | 4) { + if !is_supported_puzzle_grid_size(grid_size) { return Err(PuzzleFieldError::InvalidGridSize); } @@ -1678,19 +1763,21 @@ pub fn start_run_with_shuffle_seed_at( shuffle_seed: u64, started_at_ms: u64, ) -> Result { - let grid_size = resolve_puzzle_grid_size(cleared_level_count); + let level_index = cleared_level_count + 1; + let level_config = resolve_puzzle_level_config(level_index); + let grid_size = level_config.grid_size; let board = build_initial_board_with_seed(grid_size, shuffle_seed)?; Ok(PuzzleRunSnapshot { run_id: run_id.clone(), entry_profile_id: entry_profile.profile_id.clone(), cleared_level_count, - current_level_index: cleared_level_count + 1, + current_level_index: level_index, current_grid_size: grid_size, played_profile_ids: vec![entry_profile.profile_id.clone()], previous_level_tags: entry_profile.theme_tags.clone(), current_level: Some(PuzzleRuntimeLevelSnapshot { run_id, - level_index: cleared_level_count + 1, + level_index, level_id: entry_profile .levels .first() @@ -1706,8 +1793,8 @@ pub fn start_run_with_shuffle_seed_at( started_at_ms, cleared_at_ms: None, elapsed_ms: None, - time_limit_ms: resolve_puzzle_level_time_limit_ms(grid_size), - remaining_ms: resolve_puzzle_level_time_limit_ms(grid_size), + time_limit_ms: level_config.time_limit_ms, + remaining_ms: level_config.time_limit_ms, paused_accumulated_ms: 0, pause_started_at_ms: None, freeze_accumulated_ms: 0, @@ -1938,11 +2025,13 @@ pub fn advance_next_level_at( } let next_cleared_count = run.cleared_level_count; - let next_grid_size = resolve_puzzle_grid_size(next_cleared_count); + let next_level_index = run.current_level_index + 1; + let next_level_config = resolve_puzzle_level_config(next_level_index); + let next_grid_size = next_level_config.grid_size; let shuffle_seed = puzzle_shuffle_seed( &run.run_id, &next_profile.profile_id, - run.current_level_index + 1, + next_level_index, next_grid_size, ); let next_board = build_initial_board_with_seed(next_grid_size, shuffle_seed)?; @@ -1953,13 +2042,13 @@ pub fn advance_next_level_at( run_id: run.run_id.clone(), entry_profile_id: run.entry_profile_id.clone(), cleared_level_count: next_cleared_count, - current_level_index: run.current_level_index + 1, + current_level_index: next_level_index, current_grid_size: next_grid_size, played_profile_ids, previous_level_tags: next_profile.theme_tags.clone(), current_level: Some(PuzzleRuntimeLevelSnapshot { run_id: run.run_id.clone(), - level_index: run.current_level_index + 1, + level_index: next_level_index, level_id: next_profile .levels .first() @@ -1975,8 +2064,81 @@ pub fn advance_next_level_at( started_at_ms, cleared_at_ms: None, elapsed_ms: None, - time_limit_ms: resolve_puzzle_level_time_limit_ms(next_grid_size), - remaining_ms: resolve_puzzle_level_time_limit_ms(next_grid_size), + time_limit_ms: next_level_config.time_limit_ms, + remaining_ms: next_level_config.time_limit_ms, + 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: default_puzzle_next_level_mode(), + next_level_profile_id: None, + next_level_id: None, + recommended_next_works: Vec::new(), + leaderboard_entries: Vec::new(), + }) +} + +pub fn advance_to_new_work_first_level_at( + run: &PuzzleRunSnapshot, + next_profile: &PuzzleWorkProfile, + started_at_ms: u64, +) -> Result { + let current_level = run + .current_level + .clone() + .ok_or(PuzzleFieldError::InvalidOperation)?; + if current_level.status != PuzzleRuntimeLevelStatus::Cleared { + return Err(PuzzleFieldError::InvalidOperation); + } + + // 中文注释:跨作品代表进入一个新作品,关卡序号、切割规格和倒计时都从第 1 关重新开始。 + let next_level_index = 1; + let level_config = resolve_puzzle_level_config(next_level_index); + let grid_size = level_config.grid_size; + let shuffle_seed = puzzle_shuffle_seed( + &run.run_id, + &next_profile.profile_id, + next_level_index, + grid_size, + ); + let next_board = build_initial_board_with_seed(grid_size, shuffle_seed)?; + let mut played_profile_ids = run.played_profile_ids.clone(); + if !played_profile_ids.contains(&next_profile.profile_id) { + played_profile_ids.push(next_profile.profile_id.clone()); + } + + Ok(PuzzleRunSnapshot { + run_id: run.run_id.clone(), + entry_profile_id: next_profile.profile_id.clone(), + cleared_level_count: 0, + current_level_index: next_level_index, + current_grid_size: grid_size, + played_profile_ids, + previous_level_tags: next_profile.theme_tags.clone(), + current_level: Some(PuzzleRuntimeLevelSnapshot { + run_id: run.run_id.clone(), + level_index: next_level_index, + level_id: next_profile + .levels + .first() + .map(|level| level.level_id.clone()), + grid_size, + profile_id: next_profile.profile_id.clone(), + level_name: next_profile.level_name.clone(), + author_display_name: next_profile.author_display_name.clone(), + theme_tags: next_profile.theme_tags.clone(), + cover_image_src: next_profile.cover_image_src.clone(), + board: next_board, + status: PuzzleRuntimeLevelStatus::Playing, + started_at_ms, + cleared_at_ms: None, + elapsed_ms: None, + time_limit_ms: level_config.time_limit_ms, + remaining_ms: level_config.time_limit_ms, paused_accumulated_ms: 0, pause_started_at_ms: None, freeze_accumulated_ms: 0, @@ -2058,6 +2220,11 @@ pub fn selected_profile_level_index(profile: &PuzzleWorkProfile, level_id: &str) .position(|level| level.level_id == target_level_id) } +pub fn resolve_restart_cleared_level_count(profile: &PuzzleWorkProfile, level_id: &str) -> u32 { + // 中文注释:失败重开指定的是当前关 levelId;start_run_at 用“已通关数 + 1”计算当前关,所以这里返回关卡下标。 + selected_profile_level_index(profile, level_id).unwrap_or(0) as u32 +} + pub fn select_next_profile<'a>( current_profile: &PuzzleWorkProfile, played_profile_ids: &[String], @@ -2618,7 +2785,8 @@ fn build_initial_pieces_without_correct_neighbors( } // 随机尝试耗尽后使用确定性约束搜索兜底,保证开局没有任意一对原图相邻块互相贴边。 - let fallback_pieces = build_original_neighbor_free_pieces(grid_size, shuffle_seed) + let fallback_pieces = build_deterministic_neighbor_free_pieces(grid_size, shuffle_seed) + .or_else(|| build_original_neighbor_free_pieces(grid_size, shuffle_seed)) .unwrap_or_else(|| build_pieces_from_positions(grid_size, &base_positions)); debug_assert!(!has_any_original_neighbor_pair(&fallback_pieces)); fallback_pieces @@ -2686,6 +2854,124 @@ fn are_original_neighbors(left: &PuzzlePieceState, right: &PuzzlePieceState) -> left.correct_row.abs_diff(right.correct_row) + left.correct_col.abs_diff(right.correct_col) == 1 } +fn build_deterministic_neighbor_free_pieces( + grid_size: u32, + shuffle_seed: u64, +) -> Option> { + // 中文注释:大棋盘随机命中“无原图相邻贴边”的概率较低,失败后用确定性排列兜底保证稳定开局。 + let positions = match grid_size { + 3 => build_seeded_3x3_neighbor_free_positions(shuffle_seed), + 4 | 6 => build_affine_neighbor_free_positions(grid_size, 1, 1, 2, 1, shuffle_seed), + 5 | 7 => { + build_affine_neighbor_free_positions(grid_size, 0, 1, 2, grid_size - 1, shuffle_seed) + } + _ => return None, + }; + let pieces = build_pieces_from_positions(grid_size, &positions); + (!has_any_original_neighbor_pair(&pieces)).then_some(pieces) +} + +fn build_seeded_3x3_neighbor_free_positions(shuffle_seed: u64) -> Vec { + const LAYOUTS: [[(u32, u32); 9]; 6] = [ + [ + (0, 1), + (1, 0), + (1, 2), + (2, 0), + (0, 2), + (2, 1), + (1, 1), + (2, 2), + (0, 0), + ], + [ + (0, 1), + (1, 0), + (1, 2), + (2, 0), + (0, 2), + (2, 1), + (2, 2), + (1, 1), + (0, 0), + ], + [ + (0, 1), + (1, 0), + (1, 2), + (2, 0), + (2, 2), + (0, 0), + (1, 1), + (0, 2), + (2, 1), + ], + [ + (0, 1), + (1, 0), + (1, 2), + (2, 1), + (0, 2), + (2, 0), + (0, 0), + (2, 2), + (1, 1), + ], + [ + (0, 1), + (1, 0), + (1, 2), + (2, 2), + (0, 2), + (2, 1), + (1, 1), + (2, 0), + (0, 0), + ], + [ + (0, 1), + (1, 0), + (2, 1), + (2, 0), + (2, 2), + (0, 2), + (1, 2), + (0, 0), + (1, 1), + ], + ]; + let layout = &LAYOUTS[(shuffle_seed as usize) % LAYOUTS.len()]; + layout + .into_iter() + .map(|(row, col)| PuzzleCellPosition { + row: *row, + col: *col, + }) + .collect() +} + +fn build_affine_neighbor_free_positions( + grid_size: u32, + row_from_row: u32, + row_from_col: u32, + col_from_row: u32, + col_from_col: u32, + shuffle_seed: u64, +) -> Vec { + let row_offset = (shuffle_seed % u64::from(grid_size)) as u32; + let col_offset = ((shuffle_seed / u64::from(grid_size)) % u64::from(grid_size)) as u32; + (0..(grid_size * grid_size)) + .map(|index| { + let row = index / grid_size; + let col = index % grid_size; + PuzzleCellPosition { + row: (row_from_row * row + row_from_col * col + row_offset) % grid_size, + col: (col_from_row * row + col_from_col * col + col_offset) % grid_size, + } + }) + .collect() +} + fn build_original_neighbor_free_pieces( grid_size: u32, shuffle_seed: u64, @@ -3212,6 +3498,8 @@ mod tests { recent_play_count_7d: 0, remix_count: 0, like_count: 0, + point_incentive_total_half_points: 0, + point_incentive_claimed_points: 0, publish_ready: true, anchor_pack: empty_anchor_pack(), } @@ -3220,8 +3508,33 @@ mod tests { #[test] fn resolve_grid_size_matches_prd() { assert_eq!(resolve_puzzle_grid_size(0), 3); - assert_eq!(resolve_puzzle_grid_size(2), 3); - assert_eq!(resolve_puzzle_grid_size(3), 4); + assert_eq!(resolve_puzzle_grid_size(1), 4); + assert_eq!(resolve_puzzle_grid_size(2), 5); + assert_eq!(resolve_puzzle_grid_size(3), 5); + assert_eq!(resolve_puzzle_grid_size(4), 5); + assert_eq!(resolve_puzzle_grid_size(5), 6); + assert_eq!(resolve_puzzle_grid_size(6), 5); + assert_eq!(resolve_puzzle_grid_size(7), 7); + assert_eq!(resolve_puzzle_grid_size(8), 5); + assert_eq!(resolve_puzzle_grid_size(9), 7); + assert_eq!(resolve_puzzle_grid_size(10), 5); + assert_eq!(resolve_puzzle_grid_size(15), 7); + } + + #[test] + fn resolve_level_time_limit_matches_prd() { + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(1), 300_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(2), 300_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(3), 300_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(4), 210_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(5), 210_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(6), 240_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(7), 210_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(8), 270_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(9), 240_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(10), 270_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(11), 210_000); + assert_eq!(resolve_puzzle_level_time_limit_ms_by_index(16), 270_000); } #[test] @@ -3360,6 +3673,66 @@ mod tests { assert_eq!(selected.profile_id, "b"); } + #[test] + fn restart_cleared_count_uses_selected_level_index() { + let mut profile = build_published_profile("entry", "owner-a", vec!["机关"]); + profile.levels = vec![ + PuzzleDraftLevel { + level_id: "puzzle-level-1".to_string(), + level_name: "第一关".to_string(), + picture_description: "第一关画面".to_string(), + candidates: Vec::new(), + selected_candidate_id: None, + cover_image_src: Some("/level-1.png".to_string()), + cover_asset_id: None, + generation_status: "ready".to_string(), + }, + PuzzleDraftLevel { + level_id: "puzzle-level-2".to_string(), + level_name: "第二关".to_string(), + picture_description: "第二关画面".to_string(), + candidates: Vec::new(), + selected_candidate_id: None, + cover_image_src: Some("/level-2.png".to_string()), + cover_asset_id: None, + generation_status: "ready".to_string(), + }, + ]; + + assert_eq!( + resolve_restart_cleared_level_count(&profile, "puzzle-level-2"), + 1 + ); + assert_eq!( + resolve_restart_cleared_level_count(&profile, "missing-level"), + 0 + ); + } + + #[test] + fn advance_to_new_work_first_level_restarts_level_progress() { + let first_profile = build_published_profile("entry", "owner-a", vec!["奇幻", "遗迹"]); + let next_profile = build_published_profile("next", "owner-b", vec!["奇幻", "魔法"]); + let mut run = start_run("run-cross-work".to_string(), &first_profile, 2).expect("run"); + run.cleared_level_count = run.current_level_index; + let current_level = run.current_level.as_mut().expect("level"); + current_level.status = PuzzleRuntimeLevelStatus::Cleared; + current_level.cleared_at_ms = Some(2_000); + current_level.elapsed_ms = Some(1_000); + + let next_run = + advance_to_new_work_first_level_at(&run, &next_profile, 3_000).expect("next run"); + + assert_eq!(next_run.entry_profile_id, "next"); + assert_eq!(next_run.cleared_level_count, 0); + assert_eq!(next_run.current_level_index, 1); + let next_level = next_run.current_level.expect("next level"); + assert_eq!(next_level.profile_id, "next"); + assert_eq!(next_level.level_index, 1); + assert_eq!(next_level.grid_size, 3); + assert_eq!(next_level.time_limit_ms, 300_000); + } + #[test] fn swap_pieces_marks_cleared_when_back_to_origin() { let profile = build_published_profile("entry", "owner-a", vec!["蒸汽城市", "雨夜", "猫咪"]); @@ -3408,7 +3781,7 @@ mod tests { #[test] fn initial_board_has_no_original_neighbor_pairs() { - for grid_size in [3, 4] { + for grid_size in PUZZLE_SUPPORTED_GRID_SIZES { for shuffle_seed in 0..128 { let board = build_initial_board_with_seed(grid_size, shuffle_seed).expect("board"); @@ -3672,6 +4045,28 @@ mod tests { assert_eq!(timed_level.elapsed_ms, Some(timed_level.time_limit_ms)); } + #[test] + fn failed_level_can_extend_one_minute() { + let profile = build_published_profile("entry", "owner-a", vec!["蒸汽城市", "雨夜", "猫咪"]); + let now_ms = current_unix_ms(); + let mut run = + start_run_with_shuffle_seed("run-extend".to_string(), &profile, 0, 14).expect("run"); + let level = run.current_level.as_mut().expect("level"); + level.started_at_ms = now_ms.saturating_sub(level.time_limit_ms + 1_000); + + let failed_run = resolve_puzzle_run_timer_at(run, now_ms); + let extended_run = extend_failed_puzzle_time_at(&failed_run, now_ms + 5_000) + .expect("extend should succeed"); + let extended_level = extended_run.current_level.as_ref().expect("level"); + + assert_eq!(extended_level.status, PuzzleRuntimeLevelStatus::Playing); + assert_eq!(extended_level.remaining_ms, PUZZLE_EXTEND_TIME_DURATION_MS); + assert_eq!(extended_level.elapsed_ms, None); + assert_eq!(extended_level.cleared_at_ms, None); + assert_eq!(extended_level.pause_started_at_ms, None); + assert_eq!(extended_level.freeze_until_ms, None); + } + #[test] fn pause_and_freeze_are_excluded_from_effective_timer() { let profile = build_published_profile("entry", "owner-a", vec!["蒸汽城市", "雨夜", "猫咪"]); diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index aaa30767..7e2f0d50 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -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] diff --git a/server-rs/crates/shared-contracts/src/puzzle_works.rs b/server-rs/crates/shared-contracts/src/puzzle_works.rs index 086f3c08..cdc14bb3 100644 --- a/server-rs/crates/shared-contracts/src/puzzle_works.rs +++ b/server-rs/crates/shared-contracts/src/puzzle_works.rs @@ -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, } +#[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 { diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index 28f8510a..5d70bc2c 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -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") diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 67736666..bb49112f 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -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; diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 40db1037..60fc4aa2 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -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, } +#[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, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_work_point_incentive_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_work_point_incentive_procedure.rs new file mode 100644 index 00000000..e045794c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_work_point_incentive_procedure.rs @@ -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) + 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) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleWorkProcedureResult>( + "claim_puzzle_work_point_incentive", + ClaimPuzzleWorkPointIncentiveArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/click_match_3_d_item_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/click_match_3_d_item_procedure.rs new file mode 100644 index 00000000..3358b675 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/click_match_3_d_item_procedure.rs @@ -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) + 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) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DClickItemProcedureResult>( + "click_match_3_d_item", + ClickMatch3DItemArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/compile_match_3_d_draft_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/compile_match_3_d_draft_procedure.rs new file mode 100644 index 00000000..f7de1ebe --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/compile_match_3_d_draft_procedure.rs @@ -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) + 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) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DAgentSessionProcedureResult>( + "compile_match_3_d_draft", + CompileMatch3DDraftArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_match_3_d_agent_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_match_3_d_agent_session_procedure.rs new file mode 100644 index 00000000..48618ea1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_match_3_d_agent_session_procedure.rs @@ -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) + 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) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DAgentSessionProcedureResult>( + "create_match_3_d_agent_session", + CreateMatch3DAgentSessionArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/delete_match_3_d_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/delete_match_3_d_work_procedure.rs new file mode 100644 index 00000000..f513bf31 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/delete_match_3_d_work_procedure.rs @@ -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_work_delete_input_type::Match3DWorkDeleteInput; +use super::match_3_d_works_procedure_result_type::Match3DWorksProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct DeleteMatch3DWorkArgs { + pub input: Match3DWorkDeleteInput, +} + + +impl __sdk::InModule for DeleteMatch3DWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `delete_match_3_d_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait delete_match_3_d_work { + fn delete_match_3_d_work(&self, input: Match3DWorkDeleteInput, +) { + self.delete_match_3_d_work_then(input, |_, _| {}); + } + + fn delete_match_3_d_work_then( + &self, + input: Match3DWorkDeleteInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl delete_match_3_d_work for super::RemoteProcedures { + fn delete_match_3_d_work_then( + &self, + input: Match3DWorkDeleteInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DWorksProcedureResult>( + "delete_match_3_d_work", + DeleteMatch3DWorkArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/finalize_match_3_d_agent_message_turn_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/finalize_match_3_d_agent_message_turn_procedure.rs new file mode 100644 index 00000000..b9b416f0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/finalize_match_3_d_agent_message_turn_procedure.rs @@ -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_message_finalize_input_type::Match3DAgentMessageFinalizeInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct FinalizeMatch3DAgentMessageTurnArgs { + pub input: Match3DAgentMessageFinalizeInput, +} + + +impl __sdk::InModule for FinalizeMatch3DAgentMessageTurnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `finalize_match_3_d_agent_message_turn`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait finalize_match_3_d_agent_message_turn { + fn finalize_match_3_d_agent_message_turn(&self, input: Match3DAgentMessageFinalizeInput, +) { + self.finalize_match_3_d_agent_message_turn_then(input, |_, _| {}); + } + + fn finalize_match_3_d_agent_message_turn_then( + &self, + input: Match3DAgentMessageFinalizeInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl finalize_match_3_d_agent_message_turn for super::RemoteProcedures { + fn finalize_match_3_d_agent_message_turn_then( + &self, + input: Match3DAgentMessageFinalizeInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DAgentSessionProcedureResult>( + "finalize_match_3_d_agent_message_turn", + FinalizeMatch3DAgentMessageTurnArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/finish_match_3_d_time_up_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/finish_match_3_d_time_up_procedure.rs new file mode 100644 index 00000000..c8ce5442 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/finish_match_3_d_time_up_procedure.rs @@ -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_time_up_input_type::Match3DRunTimeUpInput; +use super::match_3_d_run_procedure_result_type::Match3DRunProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct FinishMatch3DTimeUpArgs { + pub input: Match3DRunTimeUpInput, +} + + +impl __sdk::InModule for FinishMatch3DTimeUpArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `finish_match_3_d_time_up`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait finish_match_3_d_time_up { + fn finish_match_3_d_time_up(&self, input: Match3DRunTimeUpInput, +) { + self.finish_match_3_d_time_up_then(input, |_, _| {}); + } + + fn finish_match_3_d_time_up_then( + &self, + input: Match3DRunTimeUpInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl finish_match_3_d_time_up for super::RemoteProcedures { + fn finish_match_3_d_time_up_then( + &self, + input: Match3DRunTimeUpInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DRunProcedureResult>( + "finish_match_3_d_time_up", + FinishMatch3DTimeUpArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_agent_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_agent_session_procedure.rs new file mode 100644 index 00000000..c4cb8b41 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_agent_session_procedure.rs @@ -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_get_input_type::Match3DAgentSessionGetInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetMatch3DAgentSessionArgs { + pub input: Match3DAgentSessionGetInput, +} + + +impl __sdk::InModule for GetMatch3DAgentSessionArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_match_3_d_agent_session`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_match_3_d_agent_session { + fn get_match_3_d_agent_session(&self, input: Match3DAgentSessionGetInput, +) { + self.get_match_3_d_agent_session_then(input, |_, _| {}); + } + + fn get_match_3_d_agent_session_then( + &self, + input: Match3DAgentSessionGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_match_3_d_agent_session for super::RemoteProcedures { + fn get_match_3_d_agent_session_then( + &self, + input: Match3DAgentSessionGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DAgentSessionProcedureResult>( + "get_match_3_d_agent_session", + GetMatch3DAgentSessionArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_run_procedure.rs new file mode 100644 index 00000000..41eb6be9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_run_procedure.rs @@ -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_procedure_result_type::Match3DRunProcedureResult; +use super::match_3_d_run_get_input_type::Match3DRunGetInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetMatch3DRunArgs { + pub input: Match3DRunGetInput, +} + + +impl __sdk::InModule for GetMatch3DRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_match_3_d_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_match_3_d_run { + fn get_match_3_d_run(&self, input: Match3DRunGetInput, +) { + self.get_match_3_d_run_then(input, |_, _| {}); + } + + fn get_match_3_d_run_then( + &self, + input: Match3DRunGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_match_3_d_run for super::RemoteProcedures { + fn get_match_3_d_run_then( + &self, + input: Match3DRunGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DRunProcedureResult>( + "get_match_3_d_run", + GetMatch3DRunArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_work_detail_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_work_detail_procedure.rs new file mode 100644 index 00000000..02d5b439 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_match_3_d_work_detail_procedure.rs @@ -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_work_get_input_type::Match3DWorkGetInput; +use super::match_3_d_work_procedure_result_type::Match3DWorkProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetMatch3DWorkDetailArgs { + pub input: Match3DWorkGetInput, +} + + +impl __sdk::InModule for GetMatch3DWorkDetailArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_match_3_d_work_detail`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_match_3_d_work_detail { + fn get_match_3_d_work_detail(&self, input: Match3DWorkGetInput, +) { + self.get_match_3_d_work_detail_then(input, |_, _| {}); + } + + fn get_match_3_d_work_detail_then( + &self, + input: Match3DWorkGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_match_3_d_work_detail for super::RemoteProcedures { + fn get_match_3_d_work_detail_then( + &self, + input: Match3DWorkGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DWorkProcedureResult>( + "get_match_3_d_work_detail", + GetMatch3DWorkDetailArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs index 0c78ac7c..db131f8d 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs @@ -9,8 +9,8 @@ use spacetimedb_sdk::__codegen::{ __ws, }; -use super::puzzle_work_get_input_type::PuzzleWorkGetInput; use super::puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; +use super::puzzle_work_get_input_type::PuzzleWorkGetInput; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs index c21d1048..107d90cb 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs @@ -9,8 +9,8 @@ use spacetimedb_sdk::__codegen::{ __ws, }; -use super::puzzle_work_get_input_type::PuzzleWorkGetInput; use super::puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; +use super::puzzle_work_get_input_type::PuzzleWorkGetInput; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] diff --git a/server-rs/crates/spacetime-client/src/module_bindings/list_match_3_d_works_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/list_match_3_d_works_procedure.rs new file mode 100644 index 00000000..474b43d2 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/list_match_3_d_works_procedure.rs @@ -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_works_procedure_result_type::Match3DWorksProcedureResult; +use super::match_3_d_works_list_input_type::Match3DWorksListInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct ListMatch3DWorksArgs { + pub input: Match3DWorksListInput, +} + + +impl __sdk::InModule for ListMatch3DWorksArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `list_match_3_d_works`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait list_match_3_d_works { + fn list_match_3_d_works(&self, input: Match3DWorksListInput, +) { + self.list_match_3_d_works_then(input, |_, _| {}); + } + + fn list_match_3_d_works_then( + &self, + input: Match3DWorksListInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl list_match_3_d_works for super::RemoteProcedures { + fn list_match_3_d_works_then( + &self, + input: Match3DWorksListInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DWorksProcedureResult>( + "list_match_3_d_works", + ListMatch3DWorksArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_finalize_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_finalize_input_type.rs new file mode 100644 index 00000000..ab76b76f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_finalize_input_type.rs @@ -0,0 +1,31 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DAgentMessageFinalizeInput { + pub session_id: String, + pub owner_user_id: String, + pub assistant_message_id: Option::, + pub assistant_reply_text: Option::, + pub config_json: Option::, + pub progress_percent: u32, + pub stage: String, + pub updated_at_micros: i64, + pub error_message: Option::, +} + + +impl __sdk::InModule for Match3DAgentMessageFinalizeInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_row_type.rs new file mode 100644 index 00000000..a6bd481d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_row_type.rs @@ -0,0 +1,77 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DAgentMessageRow { + pub message_id: String, + pub session_id: String, + pub role: String, + pub kind: String, + pub text: String, + pub created_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for Match3DAgentMessageRow { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `Match3DAgentMessageRow`. +/// +/// Provides typed access to columns for query building. +pub struct Match3DAgentMessageRowCols { + pub message_id: __sdk::__query_builder::Col, + pub session_id: __sdk::__query_builder::Col, + pub role: __sdk::__query_builder::Col, + pub kind: __sdk::__query_builder::Col, + pub text: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for Match3DAgentMessageRow { + type Cols = Match3DAgentMessageRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + Match3DAgentMessageRowCols { + message_id: __sdk::__query_builder::Col::new(table_name, "message_id"), + session_id: __sdk::__query_builder::Col::new(table_name, "session_id"), + role: __sdk::__query_builder::Col::new(table_name, "role"), + kind: __sdk::__query_builder::Col::new(table_name, "kind"), + text: __sdk::__query_builder::Col::new(table_name, "text"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + + } + } +} + +/// Indexed column accessor struct for the table `Match3DAgentMessageRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct Match3DAgentMessageRowIxCols { + pub message_id: __sdk::__query_builder::IxCol, + pub session_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for Match3DAgentMessageRow { + type IxCols = Match3DAgentMessageRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + Match3DAgentMessageRowIxCols { + message_id: __sdk::__query_builder::IxCol::new(table_name, "message_id"), + session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for Match3DAgentMessageRow {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_submit_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_submit_input_type.rs new file mode 100644 index 00000000..83551d6f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_submit_input_type.rs @@ -0,0 +1,27 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DAgentMessageSubmitInput { + pub session_id: String, + pub owner_user_id: String, + pub user_message_id: String, + pub user_message_text: String, + pub submitted_at_micros: i64, +} + + +impl __sdk::InModule for Match3DAgentMessageSubmitInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_create_input_type.rs new file mode 100644 index 00000000..d0e63a50 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_create_input_type.rs @@ -0,0 +1,29 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DAgentSessionCreateInput { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub welcome_message_id: String, + pub welcome_message_text: String, + pub config_json: Option::, + pub created_at_micros: i64, +} + + +impl __sdk::InModule for Match3DAgentSessionCreateInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_get_input_type.rs new file mode 100644 index 00000000..62507188 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_get_input_type.rs @@ -0,0 +1,24 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DAgentSessionGetInput { + pub session_id: String, + pub owner_user_id: String, +} + + +impl __sdk::InModule for Match3DAgentSessionGetInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_procedure_result_type.rs new file mode 100644 index 00000000..2692aee6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_procedure_result_type.rs @@ -0,0 +1,25 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DAgentSessionProcedureResult { + pub ok: bool, + pub session_json: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for Match3DAgentSessionProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_row_type.rs new file mode 100644 index 00000000..6feca688 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_row_type.rs @@ -0,0 +1,95 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DAgentSessionRow { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub config_json: String, + pub draft_json: String, + pub last_assistant_reply: String, + pub published_profile_id: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for Match3DAgentSessionRow { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `Match3DAgentSessionRow`. +/// +/// Provides typed access to columns for query building. +pub struct Match3DAgentSessionRowCols { + pub session_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub seed_text: __sdk::__query_builder::Col, + pub current_turn: __sdk::__query_builder::Col, + pub progress_percent: __sdk::__query_builder::Col, + pub stage: __sdk::__query_builder::Col, + pub config_json: __sdk::__query_builder::Col, + pub draft_json: __sdk::__query_builder::Col, + pub last_assistant_reply: __sdk::__query_builder::Col, + pub published_profile_id: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for Match3DAgentSessionRow { + type Cols = Match3DAgentSessionRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + Match3DAgentSessionRowCols { + session_id: __sdk::__query_builder::Col::new(table_name, "session_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + seed_text: __sdk::__query_builder::Col::new(table_name, "seed_text"), + current_turn: __sdk::__query_builder::Col::new(table_name, "current_turn"), + progress_percent: __sdk::__query_builder::Col::new(table_name, "progress_percent"), + stage: __sdk::__query_builder::Col::new(table_name, "stage"), + config_json: __sdk::__query_builder::Col::new(table_name, "config_json"), + draft_json: __sdk::__query_builder::Col::new(table_name, "draft_json"), + last_assistant_reply: __sdk::__query_builder::Col::new(table_name, "last_assistant_reply"), + published_profile_id: __sdk::__query_builder::Col::new(table_name, "published_profile_id"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + + } + } +} + +/// Indexed column accessor struct for the table `Match3DAgentSessionRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct Match3DAgentSessionRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub session_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for Match3DAgentSessionRow { + type IxCols = Match3DAgentSessionRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + Match3DAgentSessionRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for Match3DAgentSessionRow {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_click_item_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_click_item_procedure_result_type.rs new file mode 100644 index 00000000..a5af32a3 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_click_item_procedure_result_type.rs @@ -0,0 +1,29 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DClickItemProcedureResult { + pub ok: bool, + pub status: String, + pub run_json: Option::, + pub accepted_item_instance_id: Option::, + pub cleared_item_instance_ids: Vec::, + pub failure_reason: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for Match3DClickItemProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_draft_compile_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_draft_compile_input_type.rs new file mode 100644 index 00000000..0701e020 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_draft_compile_input_type.rs @@ -0,0 +1,32 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DDraftCompileInput { + pub session_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub author_display_name: String, + pub game_name: Option::, + pub summary_text: Option::, + pub tags_json: Option::, + pub cover_image_src: Option::, + pub cover_asset_id: Option::, + pub compiled_at_micros: i64, +} + + +impl __sdk::InModule for Match3DDraftCompileInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_click_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_click_input_type.rs new file mode 100644 index 00000000..5543b071 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_click_input_type.rs @@ -0,0 +1,28 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DRunClickInput { + pub run_id: String, + pub owner_user_id: String, + pub item_instance_id: String, + pub client_snapshot_version: u32, + pub client_event_id: String, + pub clicked_at_ms: i64, +} + + +impl __sdk::InModule for Match3DRunClickInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_get_input_type.rs new file mode 100644 index 00000000..0ca9f29e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_get_input_type.rs @@ -0,0 +1,24 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + + +impl __sdk::InModule for Match3DRunGetInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_procedure_result_type.rs new file mode 100644 index 00000000..6940b3fa --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_procedure_result_type.rs @@ -0,0 +1,25 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DRunProcedureResult { + pub ok: bool, + pub run_json: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for Match3DRunProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_restart_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_restart_input_type.rs new file mode 100644 index 00000000..95706904 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_restart_input_type.rs @@ -0,0 +1,26 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DRunRestartInput { + pub source_run_id: String, + pub next_run_id: String, + pub owner_user_id: String, + pub restarted_at_ms: i64, +} + + +impl __sdk::InModule for Match3DRunRestartInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_start_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_start_input_type.rs new file mode 100644 index 00000000..cf7b33b8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_start_input_type.rs @@ -0,0 +1,26 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DRunStartInput { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub started_at_ms: i64, +} + + +impl __sdk::InModule for Match3DRunStartInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_stop_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_stop_input_type.rs new file mode 100644 index 00000000..09277e1b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_stop_input_type.rs @@ -0,0 +1,25 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DRunStopInput { + pub run_id: String, + pub owner_user_id: String, + pub stopped_at_ms: i64, +} + + +impl __sdk::InModule for Match3DRunStopInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_time_up_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_time_up_input_type.rs new file mode 100644 index 00000000..c64316ff --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_time_up_input_type.rs @@ -0,0 +1,25 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DRunTimeUpInput { + pub run_id: String, + pub owner_user_id: String, + pub finished_at_ms: i64, +} + + +impl __sdk::InModule for Match3DRunTimeUpInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_runtime_run_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_runtime_run_row_type.rs new file mode 100644 index 00000000..32c95d31 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_runtime_run_row_type.rs @@ -0,0 +1,109 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DRuntimeRunRow { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub status: String, + pub snapshot_version: u32, + pub started_at_ms: i64, + pub duration_limit_ms: i64, + pub finished_at_ms: i64, + pub elapsed_ms: i64, + pub clear_count: u32, + pub total_item_count: u32, + pub cleared_item_count: u32, + pub failure_reason: String, + pub snapshot_json: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for Match3DRuntimeRunRow { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `Match3DRuntimeRunRow`. +/// +/// Provides typed access to columns for query building. +pub struct Match3DRuntimeRunRowCols { + pub run_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub status: __sdk::__query_builder::Col, + pub snapshot_version: __sdk::__query_builder::Col, + pub started_at_ms: __sdk::__query_builder::Col, + pub duration_limit_ms: __sdk::__query_builder::Col, + pub finished_at_ms: __sdk::__query_builder::Col, + pub elapsed_ms: __sdk::__query_builder::Col, + pub clear_count: __sdk::__query_builder::Col, + pub total_item_count: __sdk::__query_builder::Col, + pub cleared_item_count: __sdk::__query_builder::Col, + pub failure_reason: __sdk::__query_builder::Col, + pub snapshot_json: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for Match3DRuntimeRunRow { + type Cols = Match3DRuntimeRunRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + Match3DRuntimeRunRowCols { + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + status: __sdk::__query_builder::Col::new(table_name, "status"), + snapshot_version: __sdk::__query_builder::Col::new(table_name, "snapshot_version"), + started_at_ms: __sdk::__query_builder::Col::new(table_name, "started_at_ms"), + duration_limit_ms: __sdk::__query_builder::Col::new(table_name, "duration_limit_ms"), + finished_at_ms: __sdk::__query_builder::Col::new(table_name, "finished_at_ms"), + elapsed_ms: __sdk::__query_builder::Col::new(table_name, "elapsed_ms"), + clear_count: __sdk::__query_builder::Col::new(table_name, "clear_count"), + total_item_count: __sdk::__query_builder::Col::new(table_name, "total_item_count"), + cleared_item_count: __sdk::__query_builder::Col::new(table_name, "cleared_item_count"), + failure_reason: __sdk::__query_builder::Col::new(table_name, "failure_reason"), + snapshot_json: __sdk::__query_builder::Col::new(table_name, "snapshot_json"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + + } + } +} + +/// Indexed column accessor struct for the table `Match3DRuntimeRunRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct Match3DRuntimeRunRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub profile_id: __sdk::__query_builder::IxCol, + pub run_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for Match3DRuntimeRunRow { + type IxCols = Match3DRuntimeRunRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + Match3DRuntimeRunRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"), + run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for Match3DRuntimeRunRow {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_delete_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_delete_input_type.rs new file mode 100644 index 00000000..99bfcfbe --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_delete_input_type.rs @@ -0,0 +1,24 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DWorkDeleteInput { + pub profile_id: String, + pub owner_user_id: String, +} + + +impl __sdk::InModule for Match3DWorkDeleteInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_get_input_type.rs new file mode 100644 index 00000000..f83de58b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_get_input_type.rs @@ -0,0 +1,24 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DWorkGetInput { + pub profile_id: String, + pub owner_user_id: String, +} + + +impl __sdk::InModule for Match3DWorkGetInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_procedure_result_type.rs new file mode 100644 index 00000000..34963caa --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_procedure_result_type.rs @@ -0,0 +1,25 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DWorkProcedureResult { + pub ok: bool, + pub work_json: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for Match3DWorkProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_profile_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_profile_row_type.rs new file mode 100644 index 00000000..faea05a3 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_profile_row_type.rs @@ -0,0 +1,112 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DWorkProfileRow { + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub game_name: String, + pub theme_text: String, + pub summary_text: String, + pub tags_json: String, + pub cover_image_src: String, + pub cover_asset_id: String, + pub clear_count: u32, + pub difficulty: u32, + pub config_json: String, + pub publication_status: String, + pub play_count: u32, + pub updated_at: __sdk::Timestamp, + pub published_at: Option::<__sdk::Timestamp>, +} + + +impl __sdk::InModule for Match3DWorkProfileRow { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `Match3DWorkProfileRow`. +/// +/// Provides typed access to columns for query building. +pub struct Match3DWorkProfileRowCols { + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col, + pub author_display_name: __sdk::__query_builder::Col, + pub game_name: __sdk::__query_builder::Col, + pub theme_text: __sdk::__query_builder::Col, + pub summary_text: __sdk::__query_builder::Col, + pub tags_json: __sdk::__query_builder::Col, + pub cover_image_src: __sdk::__query_builder::Col, + pub cover_asset_id: __sdk::__query_builder::Col, + pub clear_count: __sdk::__query_builder::Col, + pub difficulty: __sdk::__query_builder::Col, + pub config_json: __sdk::__query_builder::Col, + pub publication_status: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, + pub published_at: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for Match3DWorkProfileRow { + type Cols = Match3DWorkProfileRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + Match3DWorkProfileRowCols { + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new(table_name, "author_display_name"), + game_name: __sdk::__query_builder::Col::new(table_name, "game_name"), + theme_text: __sdk::__query_builder::Col::new(table_name, "theme_text"), + summary_text: __sdk::__query_builder::Col::new(table_name, "summary_text"), + tags_json: __sdk::__query_builder::Col::new(table_name, "tags_json"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + cover_asset_id: __sdk::__query_builder::Col::new(table_name, "cover_asset_id"), + clear_count: __sdk::__query_builder::Col::new(table_name, "clear_count"), + difficulty: __sdk::__query_builder::Col::new(table_name, "difficulty"), + config_json: __sdk::__query_builder::Col::new(table_name, "config_json"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + published_at: __sdk::__query_builder::Col::new(table_name, "published_at"), + + } + } +} + +/// Indexed column accessor struct for the table `Match3DWorkProfileRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct Match3DWorkProfileRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub profile_id: __sdk::__query_builder::IxCol, + pub publication_status: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for Match3DWorkProfileRow { + type IxCols = Match3DWorkProfileRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + Match3DWorkProfileRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"), + publication_status: __sdk::__query_builder::IxCol::new(table_name, "publication_status"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for Match3DWorkProfileRow {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_publish_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_publish_input_type.rs new file mode 100644 index 00000000..86b32118 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_publish_input_type.rs @@ -0,0 +1,25 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DWorkPublishInput { + pub profile_id: String, + pub owner_user_id: String, + pub published_at_micros: i64, +} + + +impl __sdk::InModule for Match3DWorkPublishInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_update_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_update_input_type.rs new file mode 100644 index 00000000..f5af8f4f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_update_input_type.rs @@ -0,0 +1,33 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DWorkUpdateInput { + pub profile_id: String, + pub owner_user_id: String, + pub game_name: String, + pub theme_text: String, + pub summary_text: String, + pub tags_json: String, + pub cover_image_src: String, + pub cover_asset_id: String, + pub clear_count: u32, + pub difficulty: u32, + pub updated_at_micros: i64, +} + + +impl __sdk::InModule for Match3DWorkUpdateInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_list_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_list_input_type.rs new file mode 100644 index 00000000..82744fce --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_list_input_type.rs @@ -0,0 +1,24 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DWorksListInput { + pub owner_user_id: String, + pub published_only: bool, +} + + +impl __sdk::InModule for Match3DWorksListInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_procedure_result_type.rs new file mode 100644 index 00000000..0dcc6fe1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_procedure_result_type.rs @@ -0,0 +1,25 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DWorksProcedureResult { + pub ok: bool, + pub items_json: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for Match3DWorksProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs index bb236a52..62bb65b8 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -183,6 +183,31 @@ pub mod inventory_mutation_type; pub mod inventory_mutation_input_type; pub mod inventory_slot_type; pub mod inventory_slot_snapshot_type; +pub mod match_3_d_agent_message_finalize_input_type; +pub mod match_3_d_agent_message_row_type; +pub mod match_3_d_agent_message_submit_input_type; +pub mod match_3_d_agent_session_create_input_type; +pub mod match_3_d_agent_session_get_input_type; +pub mod match_3_d_agent_session_procedure_result_type; +pub mod match_3_d_agent_session_row_type; +pub mod match_3_d_click_item_procedure_result_type; +pub mod match_3_d_draft_compile_input_type; +pub mod match_3_d_run_click_input_type; +pub mod match_3_d_run_get_input_type; +pub mod match_3_d_run_procedure_result_type; +pub mod match_3_d_run_restart_input_type; +pub mod match_3_d_run_start_input_type; +pub mod match_3_d_run_stop_input_type; +pub mod match_3_d_run_time_up_input_type; +pub mod match_3_d_runtime_run_row_type; +pub mod match_3_d_work_delete_input_type; +pub mod match_3_d_work_get_input_type; +pub mod match_3_d_work_procedure_result_type; +pub mod match_3_d_work_profile_row_type; +pub mod match_3_d_work_publish_input_type; +pub mod match_3_d_work_update_input_type; +pub mod match_3_d_works_list_input_type; +pub mod match_3_d_works_procedure_result_type; pub mod npc_battle_interaction_procedure_result_type; pub mod npc_battle_interaction_result_type; pub mod npc_interaction_battle_mode_type; @@ -245,6 +270,7 @@ pub mod puzzle_select_cover_image_input_type; pub mod puzzle_work_delete_input_type; pub mod puzzle_work_get_input_type; pub mod puzzle_work_like_record_input_type; +pub mod puzzle_work_point_incentive_claim_input_type; pub mod puzzle_work_procedure_result_type; pub mod puzzle_work_profile_row_type; pub mod puzzle_work_remix_input_type; @@ -414,10 +440,13 @@ pub mod authorize_database_migration_operator_procedure; pub mod begin_story_session_and_return_procedure; pub mod bind_asset_object_to_entity_and_return_procedure; pub mod cancel_ai_task_and_return_procedure; +pub mod claim_puzzle_work_point_incentive_procedure; pub mod clear_database_migration_import_chunks_procedure; pub mod clear_platform_browse_history_and_return_procedure; +pub mod click_match_3_d_item_procedure; pub mod compile_big_fish_draft_procedure; pub mod compile_custom_world_published_profile_procedure; +pub mod compile_match_3_d_draft_procedure; pub mod compile_puzzle_agent_draft_procedure; pub mod complete_ai_stage_and_return_procedure; pub mod complete_ai_task_and_return_procedure; @@ -428,11 +457,13 @@ pub mod create_ai_task_and_return_procedure; pub mod create_battle_state_and_return_procedure; pub mod create_big_fish_session_procedure; pub mod create_custom_world_agent_session_procedure; +pub mod create_match_3_d_agent_session_procedure; pub mod create_profile_recharge_order_and_return_procedure; pub mod create_puzzle_agent_session_procedure; pub mod delete_big_fish_work_procedure; pub mod delete_custom_world_agent_session_procedure; pub mod delete_custom_world_profile_and_return_procedure; +pub mod delete_match_3_d_work_procedure; pub mod delete_puzzle_work_procedure; pub mod delete_runtime_snapshot_and_return_procedure; pub mod drag_puzzle_piece_or_group_procedure; @@ -442,7 +473,9 @@ pub mod export_database_migration_to_file_procedure; pub mod fail_ai_task_and_return_procedure; pub mod finalize_big_fish_agent_message_turn_procedure; pub mod finalize_custom_world_agent_message_turn_procedure; +pub mod finalize_match_3_d_agent_message_turn_procedure; pub mod finalize_puzzle_agent_message_turn_procedure; +pub mod finish_match_3_d_time_up_procedure; pub mod generate_big_fish_asset_procedure; pub mod get_auth_store_snapshot_procedure; pub mod get_battle_state_procedure; @@ -454,6 +487,9 @@ pub mod get_custom_world_agent_session_procedure; pub mod get_custom_world_gallery_detail_procedure; pub mod get_custom_world_gallery_detail_by_code_procedure; pub mod get_custom_world_library_detail_procedure; +pub mod get_match_3_d_agent_session_procedure; +pub mod get_match_3_d_run_procedure; +pub mod get_match_3_d_work_detail_procedure; pub mod get_player_progression_or_default_procedure; pub mod get_profile_dashboard_procedure; pub mod get_profile_play_stats_procedure; @@ -478,6 +514,7 @@ pub mod list_big_fish_works_procedure; pub mod list_custom_world_gallery_entries_procedure; pub mod list_custom_world_profiles_procedure; pub mod list_custom_world_works_procedure; +pub mod list_match_3_d_works_procedure; pub mod list_platform_browse_history_procedure; pub mod list_profile_save_archives_procedure; pub mod list_profile_wallet_ledger_procedure; @@ -486,6 +523,7 @@ pub mod list_puzzle_works_procedure; pub mod publish_big_fish_game_procedure; pub mod publish_custom_world_profile_and_return_procedure; pub mod publish_custom_world_world_procedure; +pub mod publish_match_3_d_work_procedure; pub mod publish_puzzle_work_procedure; pub mod put_database_migration_import_chunk_procedure; pub mod record_big_fish_like_procedure; @@ -504,18 +542,23 @@ pub mod resolve_npc_battle_interaction_and_return_procedure; pub mod resolve_npc_interaction_and_return_procedure; pub mod resolve_npc_social_action_and_return_procedure; pub mod resolve_treasure_interaction_and_return_procedure; +pub mod restart_match_3_d_run_procedure; pub mod resume_profile_save_archive_and_return_procedure; pub mod revoke_database_migration_operator_procedure; pub mod save_puzzle_form_draft_procedure; pub mod save_puzzle_generated_images_procedure; pub mod select_puzzle_cover_image_procedure; +pub mod start_match_3_d_run_procedure; pub mod start_puzzle_run_procedure; +pub mod stop_match_3_d_run_procedure; pub mod submit_big_fish_message_procedure; pub mod submit_custom_world_agent_message_procedure; +pub mod submit_match_3_d_agent_message_procedure; pub mod submit_puzzle_agent_message_procedure; pub mod submit_puzzle_leaderboard_entry_procedure; pub mod swap_puzzle_pieces_procedure; pub mod unpublish_custom_world_profile_and_return_procedure; +pub mod update_match_3_d_work_procedure; pub mod update_puzzle_run_pause_procedure; pub mod update_puzzle_work_procedure; pub mod upsert_auth_store_snapshot_procedure; @@ -700,6 +743,31 @@ pub use inventory_mutation_type::InventoryMutation; pub use inventory_mutation_input_type::InventoryMutationInput; pub use inventory_slot_type::InventorySlot; pub use inventory_slot_snapshot_type::InventorySlotSnapshot; +pub use match_3_d_agent_message_finalize_input_type::Match3DAgentMessageFinalizeInput; +pub use match_3_d_agent_message_row_type::Match3DAgentMessageRow; +pub use match_3_d_agent_message_submit_input_type::Match3DAgentMessageSubmitInput; +pub use match_3_d_agent_session_create_input_type::Match3DAgentSessionCreateInput; +pub use match_3_d_agent_session_get_input_type::Match3DAgentSessionGetInput; +pub use match_3_d_agent_session_procedure_result_type::Match3DAgentSessionProcedureResult; +pub use match_3_d_agent_session_row_type::Match3DAgentSessionRow; +pub use match_3_d_click_item_procedure_result_type::Match3DClickItemProcedureResult; +pub use match_3_d_draft_compile_input_type::Match3DDraftCompileInput; +pub use match_3_d_run_click_input_type::Match3DRunClickInput; +pub use match_3_d_run_get_input_type::Match3DRunGetInput; +pub use match_3_d_run_procedure_result_type::Match3DRunProcedureResult; +pub use match_3_d_run_restart_input_type::Match3DRunRestartInput; +pub use match_3_d_run_start_input_type::Match3DRunStartInput; +pub use match_3_d_run_stop_input_type::Match3DRunStopInput; +pub use match_3_d_run_time_up_input_type::Match3DRunTimeUpInput; +pub use match_3_d_runtime_run_row_type::Match3DRuntimeRunRow; +pub use match_3_d_work_delete_input_type::Match3DWorkDeleteInput; +pub use match_3_d_work_get_input_type::Match3DWorkGetInput; +pub use match_3_d_work_procedure_result_type::Match3DWorkProcedureResult; +pub use match_3_d_work_profile_row_type::Match3DWorkProfileRow; +pub use match_3_d_work_publish_input_type::Match3DWorkPublishInput; +pub use match_3_d_work_update_input_type::Match3DWorkUpdateInput; +pub use match_3_d_works_list_input_type::Match3DWorksListInput; +pub use match_3_d_works_procedure_result_type::Match3DWorksProcedureResult; pub use npc_battle_interaction_procedure_result_type::NpcBattleInteractionProcedureResult; pub use npc_battle_interaction_result_type::NpcBattleInteractionResult; pub use npc_interaction_battle_mode_type::NpcInteractionBattleMode; @@ -762,6 +830,7 @@ pub use puzzle_select_cover_image_input_type::PuzzleSelectCoverImageInput; pub use puzzle_work_delete_input_type::PuzzleWorkDeleteInput; pub use puzzle_work_get_input_type::PuzzleWorkGetInput; pub use puzzle_work_like_record_input_type::PuzzleWorkLikeRecordInput; +pub use puzzle_work_point_incentive_claim_input_type::PuzzleWorkPointIncentiveClaimInput; pub use puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; pub use puzzle_work_profile_row_type::PuzzleWorkProfileRow; pub use puzzle_work_remix_input_type::PuzzleWorkRemixInput; @@ -931,10 +1000,13 @@ pub use authorize_database_migration_operator_procedure::authorize_database_migr pub use begin_story_session_and_return_procedure::begin_story_session_and_return; pub use bind_asset_object_to_entity_and_return_procedure::bind_asset_object_to_entity_and_return; pub use cancel_ai_task_and_return_procedure::cancel_ai_task_and_return; +pub use claim_puzzle_work_point_incentive_procedure::claim_puzzle_work_point_incentive; pub use clear_database_migration_import_chunks_procedure::clear_database_migration_import_chunks; pub use clear_platform_browse_history_and_return_procedure::clear_platform_browse_history_and_return; +pub use click_match_3_d_item_procedure::click_match_3_d_item; pub use compile_big_fish_draft_procedure::compile_big_fish_draft; pub use compile_custom_world_published_profile_procedure::compile_custom_world_published_profile; +pub use compile_match_3_d_draft_procedure::compile_match_3_d_draft; pub use compile_puzzle_agent_draft_procedure::compile_puzzle_agent_draft; pub use complete_ai_stage_and_return_procedure::complete_ai_stage_and_return; pub use complete_ai_task_and_return_procedure::complete_ai_task_and_return; @@ -945,11 +1017,13 @@ pub use create_ai_task_and_return_procedure::create_ai_task_and_return; pub use create_battle_state_and_return_procedure::create_battle_state_and_return; pub use create_big_fish_session_procedure::create_big_fish_session; pub use create_custom_world_agent_session_procedure::create_custom_world_agent_session; +pub use create_match_3_d_agent_session_procedure::create_match_3_d_agent_session; pub use create_profile_recharge_order_and_return_procedure::create_profile_recharge_order_and_return; pub use create_puzzle_agent_session_procedure::create_puzzle_agent_session; pub use delete_big_fish_work_procedure::delete_big_fish_work; pub use delete_custom_world_agent_session_procedure::delete_custom_world_agent_session; pub use delete_custom_world_profile_and_return_procedure::delete_custom_world_profile_and_return; +pub use delete_match_3_d_work_procedure::delete_match_3_d_work; pub use delete_puzzle_work_procedure::delete_puzzle_work; pub use delete_runtime_snapshot_and_return_procedure::delete_runtime_snapshot_and_return; pub use drag_puzzle_piece_or_group_procedure::drag_puzzle_piece_or_group; @@ -959,7 +1033,9 @@ pub use export_database_migration_to_file_procedure::export_database_migration_t pub use fail_ai_task_and_return_procedure::fail_ai_task_and_return; pub use finalize_big_fish_agent_message_turn_procedure::finalize_big_fish_agent_message_turn; pub use finalize_custom_world_agent_message_turn_procedure::finalize_custom_world_agent_message_turn; +pub use finalize_match_3_d_agent_message_turn_procedure::finalize_match_3_d_agent_message_turn; pub use finalize_puzzle_agent_message_turn_procedure::finalize_puzzle_agent_message_turn; +pub use finish_match_3_d_time_up_procedure::finish_match_3_d_time_up; pub use generate_big_fish_asset_procedure::generate_big_fish_asset; pub use get_auth_store_snapshot_procedure::get_auth_store_snapshot; pub use get_battle_state_procedure::get_battle_state; @@ -971,6 +1047,9 @@ pub use get_custom_world_agent_session_procedure::get_custom_world_agent_session pub use get_custom_world_gallery_detail_procedure::get_custom_world_gallery_detail; pub use get_custom_world_gallery_detail_by_code_procedure::get_custom_world_gallery_detail_by_code; pub use get_custom_world_library_detail_procedure::get_custom_world_library_detail; +pub use get_match_3_d_agent_session_procedure::get_match_3_d_agent_session; +pub use get_match_3_d_run_procedure::get_match_3_d_run; +pub use get_match_3_d_work_detail_procedure::get_match_3_d_work_detail; pub use get_player_progression_or_default_procedure::get_player_progression_or_default; pub use get_profile_dashboard_procedure::get_profile_dashboard; pub use get_profile_play_stats_procedure::get_profile_play_stats; @@ -995,6 +1074,7 @@ pub use list_big_fish_works_procedure::list_big_fish_works; pub use list_custom_world_gallery_entries_procedure::list_custom_world_gallery_entries; pub use list_custom_world_profiles_procedure::list_custom_world_profiles; pub use list_custom_world_works_procedure::list_custom_world_works; +pub use list_match_3_d_works_procedure::list_match_3_d_works; pub use list_platform_browse_history_procedure::list_platform_browse_history; pub use list_profile_save_archives_procedure::list_profile_save_archives; pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger; @@ -1003,6 +1083,7 @@ pub use list_puzzle_works_procedure::list_puzzle_works; pub use publish_big_fish_game_procedure::publish_big_fish_game; pub use publish_custom_world_profile_and_return_procedure::publish_custom_world_profile_and_return; pub use publish_custom_world_world_procedure::publish_custom_world_world; +pub use publish_match_3_d_work_procedure::publish_match_3_d_work; pub use publish_puzzle_work_procedure::publish_puzzle_work; pub use put_database_migration_import_chunk_procedure::put_database_migration_import_chunk; pub use record_big_fish_like_procedure::record_big_fish_like; @@ -1021,18 +1102,23 @@ pub use resolve_npc_battle_interaction_and_return_procedure::resolve_npc_battle_ pub use resolve_npc_interaction_and_return_procedure::resolve_npc_interaction_and_return; pub use resolve_npc_social_action_and_return_procedure::resolve_npc_social_action_and_return; pub use resolve_treasure_interaction_and_return_procedure::resolve_treasure_interaction_and_return; +pub use restart_match_3_d_run_procedure::restart_match_3_d_run; pub use resume_profile_save_archive_and_return_procedure::resume_profile_save_archive_and_return; pub use revoke_database_migration_operator_procedure::revoke_database_migration_operator; pub use save_puzzle_form_draft_procedure::save_puzzle_form_draft; pub use save_puzzle_generated_images_procedure::save_puzzle_generated_images; pub use select_puzzle_cover_image_procedure::select_puzzle_cover_image; +pub use start_match_3_d_run_procedure::start_match_3_d_run; pub use start_puzzle_run_procedure::start_puzzle_run; +pub use stop_match_3_d_run_procedure::stop_match_3_d_run; pub use submit_big_fish_message_procedure::submit_big_fish_message; pub use submit_custom_world_agent_message_procedure::submit_custom_world_agent_message; +pub use submit_match_3_d_agent_message_procedure::submit_match_3_d_agent_message; pub use submit_puzzle_agent_message_procedure::submit_puzzle_agent_message; pub use submit_puzzle_leaderboard_entry_procedure::submit_puzzle_leaderboard_entry; pub use swap_puzzle_pieces_procedure::swap_puzzle_pieces; pub use unpublish_custom_world_profile_and_return_procedure::unpublish_custom_world_profile_and_return; +pub use update_match_3_d_work_procedure::update_match_3_d_work; pub use update_puzzle_run_pause_procedure::update_puzzle_run_pause; pub use update_puzzle_work_procedure::update_puzzle_work; pub use upsert_auth_store_snapshot_procedure::upsert_auth_store_snapshot; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/publish_match_3_d_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/publish_match_3_d_work_procedure.rs new file mode 100644 index 00000000..cb6bc86b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/publish_match_3_d_work_procedure.rs @@ -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_work_procedure_result_type::Match3DWorkProcedureResult; +use super::match_3_d_work_publish_input_type::Match3DWorkPublishInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct PublishMatch3DWorkArgs { + pub input: Match3DWorkPublishInput, +} + + +impl __sdk::InModule for PublishMatch3DWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `publish_match_3_d_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait publish_match_3_d_work { + fn publish_match_3_d_work(&self, input: Match3DWorkPublishInput, +) { + self.publish_match_3_d_work_then(input, |_, _| {}); + } + + fn publish_match_3_d_work_then( + &self, + input: Match3DWorkPublishInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl publish_match_3_d_work for super::RemoteProcedures { + fn publish_match_3_d_work_then( + &self, + input: Match3DWorkPublishInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DWorkProcedureResult>( + "publish_match_3_d_work", + PublishMatch3DWorkArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_prop_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_prop_input_type.rs index c1d70bb5..c910145d 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_prop_input_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_prop_input_type.rs @@ -17,6 +17,7 @@ pub struct PuzzleRunPropInput { pub owner_user_id: String, pub prop_kind: String, pub used_at_micros: i64, + pub spent_points: u64, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_point_incentive_claim_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_point_incentive_claim_input_type.rs new file mode 100644 index 00000000..fd375cad --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_point_incentive_claim_input_type.rs @@ -0,0 +1,25 @@ +// 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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleWorkPointIncentiveClaimInput { + pub profile_id: String, + pub owner_user_id: String, + pub claimed_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleWorkPointIncentiveClaimInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_row_type.rs index 1ec1a86f..750603e3 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_row_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_row_type.rs @@ -36,6 +36,8 @@ pub struct PuzzleWorkProfileRow { pub published_at: Option::<__sdk::Timestamp>, pub remix_count: u32, pub like_count: u32, + pub point_incentive_total_half_points: u64, + pub point_incentive_claimed_points: u64, } @@ -70,6 +72,8 @@ pub struct PuzzleWorkProfileRowCols { pub published_at: __sdk::__query_builder::Col>, pub remix_count: __sdk::__query_builder::Col, pub like_count: __sdk::__query_builder::Col, + pub point_incentive_total_half_points: __sdk::__query_builder::Col, + pub point_incentive_claimed_points: __sdk::__query_builder::Col, } impl __sdk::__query_builder::HasCols for PuzzleWorkProfileRow { @@ -98,6 +102,8 @@ impl __sdk::__query_builder::HasCols for PuzzleWorkProfileRow { published_at: __sdk::__query_builder::Col::new(table_name, "published_at"), remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"), like_count: __sdk::__query_builder::Col::new(table_name, "like_count"), + point_incentive_total_half_points: __sdk::__query_builder::Col::new(table_name, "point_incentive_total_half_points"), + point_incentive_claimed_points: __sdk::__query_builder::Col::new(table_name, "point_incentive_claimed_points"), } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/restart_match_3_d_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/restart_match_3_d_run_procedure.rs new file mode 100644 index 00000000..6ce78baa --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/restart_match_3_d_run_procedure.rs @@ -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_procedure_result_type::Match3DRunProcedureResult; +use super::match_3_d_run_restart_input_type::Match3DRunRestartInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct RestartMatch3DRunArgs { + pub input: Match3DRunRestartInput, +} + + +impl __sdk::InModule for RestartMatch3DRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `restart_match_3_d_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait restart_match_3_d_run { + fn restart_match_3_d_run(&self, input: Match3DRunRestartInput, +) { + self.restart_match_3_d_run_then(input, |_, _| {}); + } + + fn restart_match_3_d_run_then( + &self, + input: Match3DRunRestartInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl restart_match_3_d_run for super::RemoteProcedures { + fn restart_match_3_d_run_then( + &self, + input: Match3DRunRestartInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DRunProcedureResult>( + "restart_match_3_d_run", + RestartMatch3DRunArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs index 8e091481..1d8ea31d 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs @@ -27,6 +27,8 @@ pub enum RuntimeProfileWalletLedgerSourceType { RedeemCodeReward, + PuzzleAuthorIncentiveClaim, + } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/start_match_3_d_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/start_match_3_d_run_procedure.rs new file mode 100644 index 00000000..fb14b03b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/start_match_3_d_run_procedure.rs @@ -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_procedure_result_type::Match3DRunProcedureResult; +use super::match_3_d_run_start_input_type::Match3DRunStartInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct StartMatch3DRunArgs { + pub input: Match3DRunStartInput, +} + + +impl __sdk::InModule for StartMatch3DRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `start_match_3_d_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait start_match_3_d_run { + fn start_match_3_d_run(&self, input: Match3DRunStartInput, +) { + self.start_match_3_d_run_then(input, |_, _| {}); + } + + fn start_match_3_d_run_then( + &self, + input: Match3DRunStartInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl start_match_3_d_run for super::RemoteProcedures { + fn start_match_3_d_run_then( + &self, + input: Match3DRunStartInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DRunProcedureResult>( + "start_match_3_d_run", + StartMatch3DRunArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/stop_match_3_d_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/stop_match_3_d_run_procedure.rs new file mode 100644 index 00000000..a6ba49db --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/stop_match_3_d_run_procedure.rs @@ -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_procedure_result_type::Match3DRunProcedureResult; +use super::match_3_d_run_stop_input_type::Match3DRunStopInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct StopMatch3DRunArgs { + pub input: Match3DRunStopInput, +} + + +impl __sdk::InModule for StopMatch3DRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `stop_match_3_d_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait stop_match_3_d_run { + fn stop_match_3_d_run(&self, input: Match3DRunStopInput, +) { + self.stop_match_3_d_run_then(input, |_, _| {}); + } + + fn stop_match_3_d_run_then( + &self, + input: Match3DRunStopInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl stop_match_3_d_run for super::RemoteProcedures { + fn stop_match_3_d_run_then( + &self, + input: Match3DRunStopInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DRunProcedureResult>( + "stop_match_3_d_run", + StopMatch3DRunArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/submit_match_3_d_agent_message_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/submit_match_3_d_agent_message_procedure.rs new file mode 100644 index 00000000..4b32c1d0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/submit_match_3_d_agent_message_procedure.rs @@ -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_message_submit_input_type::Match3DAgentMessageSubmitInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct SubmitMatch3DAgentMessageArgs { + pub input: Match3DAgentMessageSubmitInput, +} + + +impl __sdk::InModule for SubmitMatch3DAgentMessageArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `submit_match_3_d_agent_message`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait submit_match_3_d_agent_message { + fn submit_match_3_d_agent_message(&self, input: Match3DAgentMessageSubmitInput, +) { + self.submit_match_3_d_agent_message_then(input, |_, _| {}); + } + + fn submit_match_3_d_agent_message_then( + &self, + input: Match3DAgentMessageSubmitInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl submit_match_3_d_agent_message for super::RemoteProcedures { + fn submit_match_3_d_agent_message_then( + &self, + input: Match3DAgentMessageSubmitInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DAgentSessionProcedureResult>( + "submit_match_3_d_agent_message", + SubmitMatch3DAgentMessageArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/update_match_3_d_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/update_match_3_d_work_procedure.rs new file mode 100644 index 00000000..645b544d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/update_match_3_d_work_procedure.rs @@ -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_work_procedure_result_type::Match3DWorkProcedureResult; +use super::match_3_d_work_update_input_type::Match3DWorkUpdateInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct UpdateMatch3DWorkArgs { + pub input: Match3DWorkUpdateInput, +} + + +impl __sdk::InModule for UpdateMatch3DWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `update_match_3_d_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait update_match_3_d_work { + fn update_match_3_d_work(&self, input: Match3DWorkUpdateInput, +) { + self.update_match_3_d_work_then(input, |_, _| {}); + } + + fn update_match_3_d_work_then( + &self, + input: Match3DWorkUpdateInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl update_match_3_d_work for super::RemoteProcedures { + fn update_match_3_d_work_then( + &self, + input: Match3DWorkUpdateInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, Match3DWorkProcedureResult>( + "update_match_3_d_work", + UpdateMatch3DWorkArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/puzzle.rs b/server-rs/crates/spacetime-client/src/puzzle.rs index 96cfd166..07130f84 100644 --- a/server-rs/crates/spacetime-client/src/puzzle.rs +++ b/server-rs/crates/spacetime-client/src/puzzle.rs @@ -1,5 +1,6 @@ use super::*; use crate::mapper::*; +use crate::module_bindings::claim_puzzle_work_point_incentive_procedure::claim_puzzle_work_point_incentive; use crate::module_bindings::delete_puzzle_work_procedure::delete_puzzle_work; use crate::module_bindings::record_puzzle_work_like_procedure::record_puzzle_work_like; use crate::module_bindings::remix_puzzle_work_procedure::remix_puzzle_work; @@ -340,6 +341,29 @@ impl SpacetimeClient { .await } + pub async fn claim_puzzle_work_point_incentive( + &self, + input: PuzzleWorkPointIncentiveClaimRecordInput, + ) -> Result { + let procedure_input = PuzzleWorkPointIncentiveClaimInput { + profile_id: input.profile_id, + owner_user_id: input.owner_user_id, + claimed_at_micros: input.claimed_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .claim_puzzle_work_point_incentive_then(procedure_input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_work_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + pub async fn list_puzzle_gallery( &self, ) -> Result, SpacetimeClientError> { @@ -586,6 +610,7 @@ impl SpacetimeClient { owner_user_id: input.owner_user_id, prop_kind: input.prop_kind, used_at_micros: input.used_at_micros, + spent_points: input.spent_points, }; self.call_after_connect(move |connection, sender| { diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index 61006a5b..7cd22b3d 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -1145,6 +1145,12 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde object .entry("like_count".to_string()) .or_insert_with(|| serde_json::Value::from(0)); + object + .entry("point_incentive_total_half_points".to_string()) + .or_insert_with(|| serde_json::Value::from(0)); + object + .entry("point_incentive_claimed_points".to_string()) + .or_insert_with(|| serde_json::Value::from(0)); // 中文注释:拼图多关卡字段晚于旧作品表加入,旧迁移包留空并由读取层补出首关。 object .entry("levels_json".to_string()) diff --git a/server-rs/crates/spacetime-module/src/puzzle.rs b/server-rs/crates/spacetime-module/src/puzzle.rs index 24ddc8d7..a8ed020f 100644 --- a/server-rs/crates/spacetime-module/src/puzzle.rs +++ b/server-rs/crates/spacetime-module/src/puzzle.rs @@ -1,8 +1,8 @@ use crate::runtime::{ - ProfilePlayedWorkUpsertInput, PublicWorkLikeRecordInput, PublicWorkPlayRecordInput, - ProfileSaveArchiveUpsertInput, - add_profile_observed_play_time, count_recent_public_work_plays, record_public_work_like, - record_public_work_play, upsert_profile_played_work, upsert_profile_save_archive, + ProfilePlayedWorkUpsertInput, ProfileSaveArchiveUpsertInput, PublicWorkLikeRecordInput, + PublicWorkPlayRecordInput, add_profile_observed_play_time, count_recent_public_work_plays, + grant_profile_wallet_points, record_public_work_like, record_public_work_play, + upsert_profile_played_work, upsert_profile_save_archive, }; use module_puzzle::{ PUZZLE_MAX_TAG_COUNT, PUZZLE_NEXT_LEVEL_MODE_NONE, PUZZLE_NEXT_LEVEL_MODE_SAME_WORK, @@ -16,19 +16,23 @@ use module_puzzle::{ PuzzleRunNextLevelInput, PuzzleRunPauseInput, PuzzleRunProcedureResult, PuzzleRunPropInput, PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput, PuzzleWorkDeleteInput, PuzzleWorkGetInput, - PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput, PuzzleWorkProcedureResult, PuzzleWorkProfile, - PuzzleWorkRemixInput, PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult, - apply_publish_overrides_to_draft, apply_selected_candidate, build_form_draft_from_seed, - build_result_preview, compile_result_draft_from_seed, create_work_profile, infer_anchor_pack, - normalize_puzzle_draft, normalize_puzzle_levels, normalize_theme_tags, publish_work_profile, - replace_puzzle_level, resolve_puzzle_grid_size, select_next_profiles, - selected_profile_level_after_runtime_level, selected_puzzle_level, tag_similarity_score, + PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput, PuzzleWorkPointIncentiveClaimInput, + PuzzleWorkProcedureResult, PuzzleWorkProfile, PuzzleWorkRemixInput, PuzzleWorkUpsertInput, + PuzzleWorksListInput, PuzzleWorksProcedureResult, apply_publish_overrides_to_draft, + apply_selected_candidate, build_form_draft_from_seed, build_result_preview, + compile_result_draft_from_seed, create_work_profile, infer_anchor_pack, normalize_puzzle_draft, + normalize_puzzle_levels, normalize_theme_tags, publish_work_profile, replace_puzzle_level, + select_next_profiles, selected_profile_level_after_runtime_level, selected_puzzle_level, + tag_similarity_score, }; +use module_runtime::RuntimeProfileWalletLedgerSourceType; use serde_json::from_str as json_from_str; use serde_json::json; use serde_json::to_string as json_to_string; use spacetimedb::{ProcedureContext, Table, Timestamp, TxContext}; +const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0; + /// 拼图 Agent session 真相表。 /// 当前只保存结构化字段与 JSON 草稿,不提前拆出更多编辑态子表。 #[spacetimedb::table( @@ -98,6 +102,10 @@ pub struct PuzzleWorkProfileRow { remix_count: u32, #[default(0)] like_count: u32, + #[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)] + point_incentive_total_half_points: u64, + #[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)] + point_incentive_claimed_points: u64, } /// 运行态 run 快照表。 @@ -595,6 +603,25 @@ pub fn use_puzzle_runtime_prop( } } +#[spacetimedb::procedure] +pub fn claim_puzzle_work_point_incentive( + ctx: &mut ProcedureContext, + input: PuzzleWorkPointIncentiveClaimInput, +) -> PuzzleWorkProcedureResult { + match ctx.try_with_tx(|tx| claim_puzzle_work_point_incentive_tx(tx, input.clone())) { + Ok(item) => PuzzleWorkProcedureResult { + ok: true, + item_json: Some(serialize_json(&item)), + error_message: None, + }, + Err(message) => PuzzleWorkProcedureResult { + ok: false, + item_json: None, + error_message: Some(message), + }, + } +} + #[spacetimedb::procedure] pub fn submit_puzzle_leaderboard_entry( ctx: &mut ProcedureContext, @@ -1186,6 +1213,8 @@ fn update_puzzle_work_tx( play_count: row.play_count, remix_count: row.remix_count, like_count: row.like_count, + point_incentive_total_half_points: row.point_incentive_total_half_points, + point_incentive_claimed_points: row.point_incentive_claimed_points, anchor_pack_json: row.anchor_pack_json.clone(), publish_ready: build_result_preview(&preview_draft, Some(&row.author_display_name)) .publish_ready, @@ -1341,6 +1370,8 @@ fn record_puzzle_work_like_tx( play_count: row.play_count, remix_count: row.remix_count, like_count: row.like_count.saturating_add(1), + point_incentive_total_half_points: row.point_incentive_total_half_points, + point_incentive_claimed_points: row.point_incentive_claimed_points, anchor_pack_json: row.anchor_pack_json.clone(), publish_ready: row.publish_ready, created_at: row.created_at, @@ -1427,6 +1458,8 @@ fn remix_puzzle_work_tx( play_count: source.play_count, remix_count: source.remix_count.saturating_add(1), like_count: source.like_count, + point_incentive_total_half_points: source.point_incentive_total_half_points, + point_incentive_claimed_points: source.point_incentive_claimed_points, anchor_pack_json: source.anchor_pack_json.clone(), publish_ready: source.publish_ready, created_at: source.created_at, @@ -1492,6 +1525,8 @@ fn remix_puzzle_work_tx( play_count: 0, remix_count: 0, like_count: 0, + point_incentive_total_half_points: 0, + point_incentive_claimed_points: 0, anchor_pack_json: serialize_json(&source_profile.anchor_pack), publish_ready: true, created_at: remixed_at, @@ -1531,13 +1566,20 @@ fn start_puzzle_run_tx( return Err("入口拼图作品未发布".to_string()); } let mut entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?; + let mut cleared_level_count = 0; if let Some(level) = selected_profile_level(&entry_profile, input.level_id.as_deref())? { + cleared_level_count = + module_puzzle::resolve_restart_cleared_level_count(&entry_profile, &level.level_id); entry_profile = profile_for_single_level(&entry_profile, &level); } let started_at_ms = micros_to_millis(input.started_at_micros); - let mut run = - module_puzzle::start_run_at(input.run_id.clone(), &entry_profile, 0, started_at_ms) - .map_err(|error| error.to_string())?; + let mut run = module_puzzle::start_run_at( + input.run_id.clone(), + &entry_profile, + cleared_level_count, + started_at_ms, + ) + .map_err(|error| error.to_string())?; let current_grid_size = run.current_grid_size; let current_profile_id = entry_profile.profile_id.clone(); hydrate_puzzle_leaderboard_entries( @@ -1682,26 +1724,40 @@ fn advance_puzzle_next_level_tx( .find(¤t_level.profile_id) .ok_or_else(|| "当前拼图作品不存在".to_string())?; let current_profile = build_puzzle_work_profile_from_row(¤t_profile_row)?; - let next_profile = selected_profile_level_after_runtime_level(¤t_profile, current_level) - .map(|level| profile_for_single_level(¤t_profile, &level)) - .or_else(|| { - let candidates = list_published_puzzle_profiles(ctx).ok()?; - select_next_profiles( - ¤t_profile, - ¤t_run.played_profile_ids, - &candidates, - 1, - ) - .into_iter() - .next() - .cloned() - }) + let same_work_next_profile = + selected_profile_level_after_runtime_level(¤t_profile, current_level) + .map(|level| profile_for_single_level(¤t_profile, &level)); + let similar_work_next_profile = if same_work_next_profile.is_none() { + let candidates = list_published_puzzle_profiles(ctx)?; + select_next_profiles( + ¤t_profile, + ¤t_run.played_profile_ids, + &candidates, + 1, + ) + .into_iter() + .next() + .cloned() + } else { + None + }; + let next_profile = same_work_next_profile + .as_ref() + .or(similar_work_next_profile.as_ref()) .ok_or_else(|| "没有可用的下一关候选".to_string())?; - let mut next_run = module_puzzle::advance_next_level_at( - ¤t_run, - &next_profile, - micros_to_millis(input.advanced_at_micros), - ) + let mut next_run = if same_work_next_profile.is_some() { + module_puzzle::advance_next_level_at( + ¤t_run, + next_profile, + micros_to_millis(input.advanced_at_micros), + ) + } else { + module_puzzle::advance_to_new_work_first_level_at( + ¤t_run, + next_profile, + micros_to_millis(input.advanced_at_micros), + ) + } .map_err(|error| error.to_string())?; let next_grid_size = next_run.current_grid_size; let next_profile_id = next_profile.profile_id.clone(); @@ -1805,6 +1861,19 @@ fn use_puzzle_runtime_prop_tx( }; let mut hydrated_run = next_run; refresh_next_level_handoff(ctx, &mut hydrated_run)?; + if let Some(profile_id) = hydrated_run + .current_level + .as_ref() + .map(|level| level.profile_id.clone()) + { + accrue_puzzle_point_incentive( + ctx, + &profile_id, + &input.owner_user_id, + input.spent_points, + input.used_at_micros, + )?; + } replace_puzzle_runtime_run(ctx, &row, &hydrated_run, input.used_at_micros); if let Some((profile_id, grid_size)) = hydrated_run .current_level @@ -1822,6 +1891,86 @@ fn use_puzzle_runtime_prop_tx( Ok(hydrated_run) } +fn claim_puzzle_work_point_incentive_tx( + ctx: &TxContext, + input: PuzzleWorkPointIncentiveClaimInput, +) -> Result { + let profile_id = input.profile_id.trim(); + let owner_user_id = input.owner_user_id.trim(); + if profile_id.is_empty() || owner_user_id.is_empty() { + return Err("拼图积分激励参数不能为空".to_string()); + } + + let row = ctx + .db + .puzzle_work_profile() + .profile_id() + .find(&profile_id.to_string()) + .ok_or_else(|| "拼图作品不存在".to_string())?; + if row.owner_user_id != owner_user_id { + return Err("无权领取该作品的积分激励".to_string()); + } + + let claimable_points = puzzle_point_incentive_claimable_points( + row.point_incentive_total_half_points, + row.point_incentive_claimed_points, + ); + if claimable_points == 0 { + return Err("暂无可领取积分激励".to_string()); + } + + let claimed_at = Timestamp::from_micros_since_unix_epoch(input.claimed_at_micros); + let next_row = PuzzleWorkProfileRow { + profile_id: row.profile_id.clone(), + work_id: row.work_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: row.source_session_id.clone(), + author_display_name: row.author_display_name.clone(), + work_title: row.work_title.clone(), + work_description: row.work_description.clone(), + level_name: row.level_name.clone(), + summary: row.summary.clone(), + theme_tags_json: row.theme_tags_json.clone(), + cover_image_src: row.cover_image_src.clone(), + cover_asset_id: row.cover_asset_id.clone(), + levels_json: row.levels_json.clone(), + publication_status: row.publication_status, + play_count: row.play_count, + remix_count: row.remix_count, + like_count: row.like_count, + point_incentive_total_half_points: row.point_incentive_total_half_points, + point_incentive_claimed_points: row + .point_incentive_claimed_points + .saturating_add(claimable_points), + anchor_pack_json: row.anchor_pack_json.clone(), + publish_ready: row.publish_ready, + created_at: row.created_at, + updated_at: claimed_at, + published_at: row.published_at, + }; + replace_puzzle_work_profile(ctx, &row, next_row); + + grant_profile_wallet_points( + ctx, + owner_user_id, + claimable_points, + RuntimeProfileWalletLedgerSourceType::PuzzleAuthorIncentiveClaim, + &format!( + "puzzle_author_incentive_claim:{}:{}:{}", + profile_id, owner_user_id, input.claimed_at_micros + ), + claimed_at, + )?; + + let updated = ctx + .db + .puzzle_work_profile() + .profile_id() + .find(&profile_id.to_string()) + .ok_or_else(|| "拼图积分激励领取更新失败".to_string())?; + build_puzzle_work_profile_from_row(&updated) +} + fn submit_puzzle_leaderboard_entry_tx( ctx: &TxContext, input: PuzzleLeaderboardSubmitInput, @@ -1835,7 +1984,7 @@ fn submit_puzzle_leaderboard_entry_tx( if input.profile_id.trim().is_empty() { return Err("提交成绩的拼图作品不能为空".to_string()); } - if input.grid_size != 3 && input.grid_size != 4 { + if !module_puzzle::is_supported_puzzle_grid_size(input.grid_size) { return Err("提交成绩的网格规格无效".to_string()); } let matches_service_level = @@ -2002,6 +2151,8 @@ fn build_puzzle_work_profile_from_row_without_recent_count( play_count: row.play_count, remix_count: row.remix_count, like_count: row.like_count, + point_incentive_total_half_points: row.point_incentive_total_half_points, + point_incentive_claimed_points: row.point_incentive_claimed_points, recent_play_count_7d: 0, publish_ready: row.publish_ready, anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?, @@ -2108,6 +2259,8 @@ fn upsert_puzzle_draft_work_profile( profile.play_count = existing.play_count; profile.remix_count = existing.remix_count; profile.like_count = existing.like_count; + profile.point_incentive_total_half_points = existing.point_incentive_total_half_points; + profile.point_incentive_claimed_points = existing.point_incentive_claimed_points; return upsert_puzzle_work_profile(ctx, profile); } let profile = create_work_profile( @@ -2286,6 +2439,12 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re play_count: existing.play_count.max(profile.play_count), remix_count: existing.remix_count.max(profile.remix_count), like_count: existing.like_count.max(profile.like_count), + point_incentive_total_half_points: existing + .point_incentive_total_half_points + .max(profile.point_incentive_total_half_points), + point_incentive_claimed_points: existing + .point_incentive_claimed_points + .max(profile.point_incentive_claimed_points), anchor_pack_json: serialize_json(&profile.anchor_pack), publish_ready: profile.publish_ready, created_at: existing.created_at, @@ -2316,6 +2475,8 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re play_count: profile.play_count, remix_count: profile.remix_count, like_count: profile.like_count, + point_incentive_total_half_points: profile.point_incentive_total_half_points, + point_incentive_claimed_points: profile.point_incentive_claimed_points, anchor_pack_json: serialize_json(&profile.anchor_pack), publish_ready: profile.publish_ready, created_at: Timestamp::from_micros_since_unix_epoch(profile.updated_at_micros), @@ -2375,7 +2536,7 @@ fn replace_puzzle_runtime_run( .unwrap_or_else(|| current.current_profile_id.clone()), cleared_level_count: run.cleared_level_count, current_level_index: run.current_level_index, - current_grid_size: resolve_puzzle_grid_size(run.cleared_level_count), + current_grid_size: run.current_grid_size, played_profile_ids_json: serialize_json(&run.played_profile_ids), previous_level_tags_json: serialize_json(&run.previous_level_tags), snapshot_json: serialize_json(run), @@ -2403,16 +2564,17 @@ fn upsert_puzzle_profile_save_archive( return Ok(()); }; let world_key = format!("puzzle:{}", run.entry_profile_id); + let target = resolve_puzzle_archive_target(ctx, run, current_level)?; // 中文注释:拼图存档只保存恢复入口所需的最小运行态索引,棋盘真相继续放在 puzzle_runtime_run。 let game_state_json = json_to_string(&json!({ "runtimeKind": "puzzle", "runId": run.run_id, "entryProfileId": run.entry_profile_id, - "currentProfileId": current_level.profile_id, - "currentLevelIndex": current_level.level_index, - "currentLevelId": current_level.level_id, - "status": current_level.status.as_str(), + "currentProfileId": target.profile_id, + "currentLevelIndex": target.level_index, + "currentLevelId": target.level_id, + "status": target.status.as_str(), })) .unwrap_or_else(|_| "{}".to_string()); @@ -2421,13 +2583,13 @@ fn upsert_puzzle_profile_save_archive( ProfileSaveArchiveUpsertInput { user_id: user_id.to_string(), world_key, - owner_user_id: resolve_puzzle_current_owner_user_id(ctx, ¤t_level.profile_id), + owner_user_id: target.owner_user_id, profile_id: Some(run.entry_profile_id.clone()), world_type: Some("PUZZLE".to_string()), - world_name: current_level.level_name.clone(), - subtitle: format!("第 {} 关", current_level.level_index), - summary_text: puzzle_archive_summary_text(current_level.status), - cover_image_src: current_level.cover_image_src.clone(), + world_name: target.level_name, + subtitle: format!("第 {} 关", target.level_index), + summary_text: puzzle_archive_summary_text(target.status), + cover_image_src: target.cover_image_src, bottom_tab: "puzzle".to_string(), game_state_json, current_story_json: None, @@ -2436,6 +2598,88 @@ fn upsert_puzzle_profile_save_archive( ) } +struct PuzzleArchiveTarget { + profile_id: String, + level_index: u32, + level_id: Option, + level_name: String, + status: PuzzleRuntimeLevelStatus, + cover_image_src: Option, + owner_user_id: Option, +} + +fn resolve_puzzle_archive_target( + ctx: &TxContext, + run: &PuzzleRunSnapshot, + current_level: &module_puzzle::PuzzleRuntimeLevelSnapshot, +) -> Result { + let owner_user_id = resolve_puzzle_current_owner_user_id(ctx, ¤t_level.profile_id); + if current_level.status != PuzzleRuntimeLevelStatus::Cleared { + return Ok(PuzzleArchiveTarget { + profile_id: current_level.profile_id.clone(), + level_index: current_level.level_index, + level_id: current_level.level_id.clone(), + level_name: current_level.level_name.clone(), + status: current_level.status, + cover_image_src: current_level.cover_image_src.clone(), + owner_user_id, + }); + } + + let Some(next_level_id) = run + .next_level_id + .as_deref() + .filter(|value| !value.trim().is_empty()) + else { + return Ok(PuzzleArchiveTarget { + profile_id: current_level.profile_id.clone(), + level_index: current_level.level_index, + level_id: current_level.level_id.clone(), + level_name: current_level.level_name.clone(), + status: current_level.status, + cover_image_src: current_level.cover_image_src.clone(), + owner_user_id, + }); + }; + if run.next_level_profile_id.as_deref() != Some(current_level.profile_id.as_str()) + || run.next_level_mode != PUZZLE_NEXT_LEVEL_MODE_SAME_WORK + { + return Ok(PuzzleArchiveTarget { + profile_id: current_level.profile_id.clone(), + level_index: current_level.level_index, + level_id: current_level.level_id.clone(), + level_name: current_level.level_name.clone(), + status: current_level.status, + cover_image_src: current_level.cover_image_src.clone(), + owner_user_id, + }); + } + + let current_profile = build_puzzle_work_profile_from_row( + &ctx.db + .puzzle_work_profile() + .profile_id() + .find(¤t_level.profile_id) + .ok_or_else(|| "当前拼图作品不存在".to_string())?, + )?; + let next_level = current_profile + .levels + .iter() + .find(|level| level.level_id == next_level_id) + .cloned() + .ok_or_else(|| "下一关拼图关卡不存在".to_string())?; + + Ok(PuzzleArchiveTarget { + profile_id: current_profile.profile_id, + level_index: current_level.level_index.saturating_add(1), + level_id: Some(next_level.level_id), + level_name: next_level.level_name, + status: PuzzleRuntimeLevelStatus::Playing, + cover_image_src: next_level.cover_image_src, + owner_user_id, + }) +} + fn resolve_puzzle_current_owner_user_id(ctx: &TxContext, profile_id: &str) -> Option { ctx.db .puzzle_work_profile() @@ -2453,6 +2697,72 @@ fn puzzle_archive_summary_text(status: PuzzleRuntimeLevelStatus) -> String { .to_string() } +fn puzzle_point_incentive_claimable_points(total_half_points: u64, claimed_points: u64) -> u64 { + total_half_points + .saturating_div(2) + .saturating_sub(claimed_points) +} + +fn accrue_puzzle_point_incentive( + ctx: &TxContext, + profile_id: &str, + player_user_id: &str, + spent_points: u64, + updated_at_micros: i64, +) -> Result<(), String> { + if spent_points == 0 { + return Ok(()); + } + + let Some(row) = ctx + .db + .puzzle_work_profile() + .profile_id() + .find(&profile_id.to_string()) + else { + return Ok(()); + }; + if row.publication_status != PuzzlePublicationStatus::Published + || row.owner_user_id == player_user_id + { + return Ok(()); + } + + replace_puzzle_work_profile( + ctx, + &row, + PuzzleWorkProfileRow { + profile_id: row.profile_id.clone(), + work_id: row.work_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: row.source_session_id.clone(), + author_display_name: row.author_display_name.clone(), + work_title: row.work_title.clone(), + work_description: row.work_description.clone(), + level_name: row.level_name.clone(), + summary: row.summary.clone(), + theme_tags_json: row.theme_tags_json.clone(), + cover_image_src: row.cover_image_src.clone(), + cover_asset_id: row.cover_asset_id.clone(), + levels_json: row.levels_json.clone(), + publication_status: row.publication_status, + play_count: row.play_count, + remix_count: row.remix_count, + like_count: row.like_count, + point_incentive_total_half_points: row + .point_incentive_total_half_points + .saturating_add(spent_points), + point_incentive_claimed_points: row.point_incentive_claimed_points, + anchor_pack_json: row.anchor_pack_json.clone(), + publish_ready: row.publish_ready, + created_at: row.created_at, + updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros), + published_at: row.published_at, + }, + ); + Ok(()) +} + fn increment_puzzle_profile_play_count( ctx: &TxContext, row: &PuzzleWorkProfileRow, @@ -2479,6 +2789,8 @@ fn increment_puzzle_profile_play_count( play_count: row.play_count.saturating_add(1), remix_count: row.remix_count, like_count: row.like_count, + point_incentive_total_half_points: row.point_incentive_total_half_points, + point_incentive_claimed_points: row.point_incentive_claimed_points, anchor_pack_json: row.anchor_pack_json.clone(), publish_ready: row.publish_ready, created_at: row.created_at, @@ -2841,7 +3153,7 @@ mod tests { }]; replace_generated_candidate( - &mut draft, + &mut draft.candidates, vec![PuzzleGeneratedImageCandidate { candidate_id: "session-1-candidate-2".to_string(), image_src: "/generated-puzzle-assets/session-1/new/cover.png".to_string(), @@ -2866,11 +3178,14 @@ mod tests { owner_user_id: "owner-a".to_string(), source_session_id: None, author_display_name: "作者".to_string(), + work_title: "A".to_string(), + work_description: String::new(), level_name: "A".to_string(), summary: String::new(), theme_tags: vec!["雨夜".to_string(), "猫咪".to_string()], cover_image_src: Some("/a.png".to_string()), cover_asset_id: Some("asset-a".to_string()), + levels: Vec::new(), publication_status: PuzzlePublicationStatus::Published, updated_at_micros: 1, published_at_micros: Some(1), @@ -2878,6 +3193,8 @@ mod tests { recent_play_count_7d: 0, remix_count: 0, like_count: 0, + point_incentive_total_half_points: 0, + point_incentive_claimed_points: 0, publish_ready: true, anchor_pack: empty_anchor_pack(), }; @@ -2885,10 +3202,13 @@ mod tests { owner_user_id: "owner-a".to_string(), profile_id: "profile-b".to_string(), work_id: "work-b".to_string(), + work_title: "B".to_string(), + work_description: String::new(), level_name: "B".to_string(), theme_tags: vec!["雨夜".to_string(), "蒸汽城市".to_string()], cover_image_src: Some("/b.png".to_string()), cover_asset_id: Some("asset-b".to_string()), + levels: Vec::new(), publication_status: PuzzlePublicationStatus::Published, updated_at_micros: 2, published_at_micros: Some(2), @@ -2896,6 +3216,8 @@ mod tests { recent_play_count_7d: 0, remix_count: 0, like_count: 0, + point_incentive_total_half_points: 0, + point_incentive_claimed_points: 0, publish_ready: true, anchor_pack: empty_anchor_pack(), source_session_id: None, diff --git a/server-rs/crates/spacetime-module/src/runtime/profile.rs b/server-rs/crates/spacetime-module/src/runtime/profile.rs index 261c3040..f99494fc 100644 --- a/server-rs/crates/spacetime-module/src/runtime/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/profile.rs @@ -2120,6 +2120,24 @@ fn apply_profile_wallet_delta( ) } +pub(crate) fn grant_profile_wallet_points( + ctx: &ReducerContext, + user_id: &str, + amount_delta: u64, + source_type: RuntimeProfileWalletLedgerSourceType, + ledger_id: &str, + created_at: Timestamp, +) -> Result { + apply_profile_wallet_delta( + ctx, + user_id, + amount_delta, + source_type, + ledger_id, + created_at, + ) +} + fn apply_profile_wallet_adjustment( ctx: &ReducerContext, input: RuntimeProfileWalletAdjustmentInput, diff --git a/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx b/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx index 304aa08f..e884bb95 100644 --- a/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx +++ b/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx @@ -190,6 +190,59 @@ test('creation hub mixes puzzle works into the same grid and uses puzzle tag to expect(screen.queryByText('我的拼图作品')).toBeNull(); }); +test('creation hub shows puzzle point incentive and claims without opening card', async () => { + const user = userEvent.setup(); + const onClaimPuzzlePointIncentive = vi.fn(); + const onOpenPuzzleDetail = vi.fn(); + + render( + {}} + onCreateType={noopCreateType} + onOpenDraft={() => {}} + onEnterPublished={() => {}} + onOpenPuzzleDetail={onOpenPuzzleDetail} + onClaimPuzzlePointIncentive={onClaimPuzzlePointIncentive} + />, + ); + + expect(screen.getByLabelText('积分激励总数 2.5 陶泥币')).toBeTruthy(); + expect(screen.getByLabelText('待领取积分 1 陶泥币')).toBeTruthy(); + + await user.click(screen.getByRole('button', { name: '领取积分' })); + + expect(onClaimPuzzlePointIncentive).toHaveBeenCalledWith( + expect.objectContaining({ profileId: 'puzzle-profile-incentive' }), + ); + expect(onOpenPuzzleDetail).not.toHaveBeenCalled(); +}); + test('creation hub shows RPG public work code from published library entry', () => { render( void; onDeletePuzzle?: ((item: PuzzleWorkSummary) => void) | null; + onClaimPuzzlePointIncentive?: ((item: PuzzleWorkSummary) => void) | null; + claimingPuzzleProfileId?: string | null; }; function EmptyState({ title }: { title: string }) { @@ -131,6 +133,8 @@ export function CustomWorldCreationHub({ puzzleItems = [], onOpenPuzzleDetail, onDeletePuzzle = null, + onClaimPuzzlePointIncentive = null, + claimingPuzzleProfileId = null, }: CustomWorldCreationHubProps) { const [activeFilter, setActiveFilter] = useState('all'); @@ -222,6 +226,17 @@ export function CustomWorldCreationHub({ } } + function buildPointIncentiveAction(item: CreationWorkShelfItem) { + if (item.source.kind !== 'puzzle' || !onClaimPuzzlePointIncentive) { + return null; + } + + const sourceItem = item.source.item; + return () => { + onClaimPuzzlePointIncentive(sourceItem); + }; + } + return (
@@ -281,6 +296,11 @@ export function CustomWorldCreationHub({ onOpen={() => handleOpenShelfItem(item)} onDelete={buildDeleteAction(item)} deleteBusy={deletingWorkId === item.id} + onClaimPointIncentive={buildPointIncentiveAction(item)} + pointIncentiveBusy={ + item.source.kind === 'puzzle' && + claimingPuzzleProfileId === item.source.item.profileId + } /> ))}
diff --git a/src/components/custom-world-home/CustomWorldWorkCard.tsx b/src/components/custom-world-home/CustomWorldWorkCard.tsx index 41ecdf64..4b3036e9 100644 --- a/src/components/custom-world-home/CustomWorldWorkCard.tsx +++ b/src/components/custom-world-home/CustomWorldWorkCard.tsx @@ -13,6 +13,7 @@ import { type CreationWorkShelfMetric, type CreationWorkShelfMetricId, formatCreationMetricCount, + formatCreationPointIncentiveTotal, } from './creationWorkShelf'; type CustomWorldWorkCardProps = { @@ -21,6 +22,8 @@ type CustomWorldWorkCardProps = { onOpen: () => void; onDelete?: (() => void) | null; deleteBusy?: boolean; + onClaimPointIncentive?: (() => void) | null; + pointIncentiveBusy?: boolean; }; const BADGE_TONE_CLASS: Record = { @@ -189,12 +192,17 @@ export function CustomWorldWorkCard({ onOpen, onDelete = null, deleteBusy = false, + onClaimPointIncentive = null, + pointIncentiveBusy = false, }: CustomWorldWorkCardProps) { const [shareState, setShareState] = useState<'idle' | 'copied' | 'failed'>( 'idle', ); const shareResetTimerRef = useRef(null); const isPublished = item.status === 'published'; + const canClaimPointIncentive = + Boolean(onClaimPointIncentive) && + (item.pointIncentive?.claimablePoints ?? 0) > 0; const displayTitle = formatPlatformWorkDisplayName(item.title); const { cardRef, deltas, displayValues, showGrowth } = usePublishedMetricAnimation( @@ -346,34 +354,81 @@ export function CustomWorldWorkCard({
{isPublished ? ( -
- {item.metrics.map((metric) => ( -
- - {metric.label} - - - - {formatCreationMetricCount( - displayValues[metric.id] ?? metric.value, +
+ {item.pointIncentive ? ( +
+
+ + 积分激励 + + + {formatCreationPointIncentiveTotal( + item.pointIncentive.totalPoints, )} - - {metric.unit} +
+
+ + 待领取 - - {showGrowth && deltas[metric.id] > 0 ? ( - - - {formatCreationMetricCount(deltas[metric.id])} + + {formatCreationMetricCount( + item.pointIncentive.claimablePoints, + )} - ) : null} +
+
- ))} + ) : null} + +
+ {item.metrics.map((metric) => ( +
+ + {metric.label} + + + + {formatCreationMetricCount( + displayValues[metric.id] ?? metric.value, + )} + + + {metric.unit} + + + {showGrowth && deltas[metric.id] > 0 ? ( + + + {formatCreationMetricCount(deltas[metric.id])} + + ) : null} +
+ ))} +
) : null}
diff --git a/src/components/custom-world-home/creationWorkShelf.ts b/src/components/custom-world-home/creationWorkShelf.ts index 4b51f1e7..3be8e6db 100644 --- a/src/components/custom-world-home/creationWorkShelf.ts +++ b/src/components/custom-world-home/creationWorkShelf.ts @@ -35,6 +35,12 @@ export type CreationWorkShelfMetric = { tone: CreationWorkShelfMetricTone; }; +export type CreationWorkShelfPointIncentive = { + totalHalfPoints: number; + totalPoints: number; + claimablePoints: number; +}; + export type CreationWorkShelfSource = | { kind: 'rpg'; @@ -66,6 +72,7 @@ export type CreationWorkShelfItem = { canShare: boolean; badges: CreationWorkShelfBadge[]; metrics: CreationWorkShelfMetric[]; + pointIncentive?: CreationWorkShelfPointIncentive; source: CreationWorkShelfSource; }; @@ -238,6 +245,21 @@ function mapPuzzleWorkToShelfItem( likeCount: item.likeCount, }) : [], + pointIncentive: + status === 'published' + ? { + totalHalfPoints: normalizeMetricCount( + item.pointIncentiveTotalHalfPoints, + ), + totalPoints: normalizePointIncentiveTotal( + item.pointIncentiveTotalPoints, + item.pointIncentiveTotalHalfPoints, + ), + claimablePoints: normalizeMetricCount( + item.pointIncentiveClaimablePoints, + ), + } + : undefined, source: { kind: 'puzzle', item }, }; } @@ -286,6 +308,24 @@ export function formatCreationMetricCount(value?: number | null) { return `${normalized}`; } +export function formatCreationPointIncentiveTotal(value?: number | null) { + const normalized = Math.max(0, value ?? 0); + return Number.isInteger(normalized) + ? normalized.toFixed(0) + : normalized.toFixed(1); +} + +function normalizePointIncentiveTotal( + totalPoints?: number | null, + totalHalfPoints?: number | null, +) { + if (Number.isFinite(totalPoints)) { + return Math.max(0, totalPoints ?? 0); + } + + return normalizeMetricCount(totalHalfPoints) / 2; +} + function buildStatusBadge( status: CreationWorkShelfStatus, ): CreationWorkShelfBadge { diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 63de41d5..49ce361c 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -48,6 +48,7 @@ import type { CustomWorldLibraryEntry, ProfilePlayedWorkSummary, ProfilePlayStatsResponse, + ProfileSaveArchiveResumeResponse, ProfileSaveArchiveSummary, } from '../../../packages/shared/src/contracts/runtime'; import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets'; @@ -125,13 +126,18 @@ import { extendLocalPuzzleTime, isLocalPuzzleRun, refreshLocalPuzzleTimer, + resolvePuzzleRestartLevelId, restartLocalPuzzleLevel, setLocalPuzzlePaused, startLocalPuzzleRun, submitLocalPuzzleLeaderboard, swapLocalPuzzlePieces, } from '../../services/puzzle-runtime/puzzleLocalRuntime'; -import { deletePuzzleWork, listPuzzleWorks } from '../../services/puzzle-works'; +import { + claimPuzzleWorkPointIncentive, + deletePuzzleWork, + listPuzzleWorks, +} from '../../services/puzzle-works'; import { deleteRpgCreationAgentSession } from '../../services/rpg-creation'; import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter'; import { @@ -141,10 +147,8 @@ import { recordRpgEntryWorldGalleryPlay, remixRpgEntryWorldGallery, } from '../../services/rpg-entry/rpgEntryLibraryClient'; -import { - getRpgProfilePlayStats, - resumeRpgProfileSaveArchive, -} from '../../services/rpg-entry/rpgProfileClient'; +import { getRpgProfilePlayStats } from '../../services/rpg-entry/rpgProfileClient'; +import { requestRpgRuntimeJson } from '../../services/rpg-runtime/rpgRuntimeRequest'; import type { CustomWorldProfile } from '../../types'; import { useAuthUi } from '../auth/AuthUiContext'; import { @@ -201,6 +205,16 @@ type PuzzleSaveArchiveState = { currentLevelId?: unknown; }; +async function resumePuzzleProfileSaveArchiveRaw(worldKey: string) { + return requestRpgRuntimeJson< + ProfileSaveArchiveResumeResponse + >( + `/profile/save-archives/${encodeURIComponent(worldKey)}`, + { method: 'POST' }, + '恢复拼图存档失败', + ); +} + type AgentResultBlockerView = { code?: string; message: string; @@ -297,6 +311,10 @@ function mapPublicWorkDetailToPuzzleWork( playCount: entry.playCount ?? 0, remixCount: entry.remixCount ?? 0, likeCount: entry.likeCount ?? 0, + pointIncentiveTotalHalfPoints: 0, + pointIncentiveClaimedPoints: 0, + pointIncentiveTotalPoints: 0, + pointIncentiveClaimablePoints: 0, publishReady: true, levels: entry.coverSlides?.map((slide, index) => ({ @@ -729,11 +747,10 @@ function mergePuzzleServiceRuntimeState( } const serviceLevel = serviceRun.currentLevel; - const leaderboardEntries = - serviceLevel.leaderboardEntries.length > 0 - ? serviceLevel.leaderboardEntries - : serviceRun.leaderboardEntries; - + if ( + currentRun.currentLevel.status === 'cleared' && + serviceLevel.status !== 'cleared' + ) { return { ...currentRun, recommendedNextProfileId: serviceRun.recommendedNextProfileId, @@ -741,8 +758,27 @@ function mergePuzzleServiceRuntimeState( nextLevelProfileId: serviceRun.nextLevelProfileId, nextLevelId: serviceRun.nextLevelId, recommendedNextWorks: serviceRun.recommendedNextWorks, - leaderboardEntries, - currentLevel: { + leaderboardEntries: + currentRun.currentLevel.leaderboardEntries.length > 0 + ? currentRun.currentLevel.leaderboardEntries + : currentRun.leaderboardEntries, + }; + } + + const leaderboardEntries = + serviceLevel.leaderboardEntries.length > 0 + ? serviceLevel.leaderboardEntries + : serviceRun.leaderboardEntries; + + return { + ...currentRun, + recommendedNextProfileId: serviceRun.recommendedNextProfileId, + nextLevelMode: serviceRun.nextLevelMode, + nextLevelProfileId: serviceRun.nextLevelProfileId, + nextLevelId: serviceRun.nextLevelId, + recommendedNextWorks: serviceRun.recommendedNextWorks, + leaderboardEntries, + currentLevel: { ...currentRun.currentLevel, status: serviceLevel.status, startedAtMs: serviceLevel.startedAtMs, @@ -836,6 +872,8 @@ export function PlatformEntryFlowShellImpl({ const [deletingCreationWorkId, setDeletingCreationWorkId] = useState< string | null >(null); + const [claimingPuzzlePointIncentiveProfileId, setClaimingPuzzlePointIncentiveProfileId] = + useState(null); const isBigFishCreationVisible = isPlatformCreationTypeVisible('big-fish'); const [profilePlayStats, setProfilePlayStats] = useState(null); @@ -1569,6 +1607,7 @@ export function PlatformEntryFlowShellImpl({ setIsPuzzleNextLevelGenerating(false); setPuzzleError(null); setDeletingCreationWorkId(null); + setClaimingPuzzlePointIncentiveProfileId(null); setProfilePlayStats(null); setProfilePlayStatsError(null); setIsProfilePlayStatsOpen(false); @@ -1812,6 +1851,7 @@ export function PlatformEntryFlowShellImpl({ setPuzzleRun(run); setPuzzleRuntimeReturnStage(returnStage); setSelectionStage('puzzle-runtime'); + void platformBootstrap.refreshSaveArchives(); pushAppHistoryPath( buildPublicWorkStagePath( 'puzzle-runtime', @@ -1830,6 +1870,7 @@ export function PlatformEntryFlowShellImpl({ }, [ isPuzzleBusy, + platformBootstrap, resolvePuzzleErrorMessage, setIsPuzzleBusy, setPuzzleError, @@ -1863,6 +1904,10 @@ export function PlatformEntryFlowShellImpl({ playCount: 0, remixCount: 0, likeCount: 0, + pointIncentiveTotalHalfPoints: 0, + pointIncentiveClaimedPoints: 0, + pointIncentiveTotalPoints: 0, + pointIncentiveClaimablePoints: 0, publishReady: Boolean(puzzleSession?.resultPreview?.publishReady), levels: draft.levels, } satisfies PuzzleWorkSummary; @@ -1963,9 +2008,7 @@ export function PlatformEntryFlowShellImpl({ } const timerId = window.setInterval(() => { - if (!isLocalPuzzleRun(puzzleRun)) { - return; - } + // 中文注释:正式 run 的棋盘交互也在前端即时裁决,倒计时展示同样走本地时钟;超时落库仍由 onTimeExpired 拉取后端快照完成。 setPuzzleRun((currentRun) => currentRun ? refreshLocalPuzzleTimer(currentRun) : currentRun, ); @@ -2009,7 +2052,7 @@ export function PlatformEntryFlowShellImpl({ const syncPuzzleRuntimeTimeout = useCallback(async () => { if ( !puzzleRun?.currentLevel || - puzzleRun.currentLevel.status !== 'playing' + puzzleRun.currentLevel.status === 'cleared' ) { return; } @@ -2040,9 +2083,11 @@ export function PlatformEntryFlowShellImpl({ if (!puzzleRun?.currentLevel) { return null; } - const expectedStatus = - propKind === 'extendTime' ? 'failed' : 'playing'; - if (puzzleRun.currentLevel.status !== expectedStatus) { + const canUseProp = + propKind === 'extendTime' + ? puzzleRun.currentLevel.status !== 'cleared' + : puzzleRun.currentLevel.status === 'playing'; + if (!canUseProp) { return null; } @@ -2072,6 +2117,7 @@ export function PlatformEntryFlowShellImpl({ puzzleRunRef.current = nextRun; setPuzzleRun(nextRun); void platformBootstrap.refreshProfileDashboard(); + void platformBootstrap.refreshSaveArchives(); return nextRun; }, [platformBootstrap, puzzleRun], @@ -2084,6 +2130,10 @@ export function PlatformEntryFlowShellImpl({ } setPuzzleError(null); + const restartLevelId = resolvePuzzleRestartLevelId( + puzzleRun, + selectedPuzzleDetail, + ); if (isLocalPuzzleRun(puzzleRun)) { const nextRun = restartLocalPuzzleLevel(puzzleRunRef.current ?? puzzleRun); puzzleRunRef.current = nextRun; @@ -2098,7 +2148,7 @@ export function PlatformEntryFlowShellImpl({ ? selectedPuzzleDetail : undefined, false, - currentLevel.levelId ?? null, + restartLevelId, ); }, [ isPuzzleBusy, @@ -2120,7 +2170,7 @@ export function PlatformEntryFlowShellImpl({ platformBootstrap.setSaveError(null); try { - const resumedArchive = await resumeRpgProfileSaveArchive( + const resumedArchive = await resumePuzzleProfileSaveArchiveRaw( entry.worldKey, ); platformBootstrap.setSaveEntries((currentEntries) => @@ -2130,8 +2180,7 @@ export function PlatformEntryFlowShellImpl({ : currentEntry, ), ); - const gameState = resumedArchive.snapshot - .gameState as PuzzleSaveArchiveState; + const gameState = resumedArchive.snapshot.gameState; const profileId = typeof gameState.currentProfileId === 'string' && gameState.currentProfileId.trim() @@ -2145,7 +2194,13 @@ export function PlatformEntryFlowShellImpl({ gameState.currentLevelId.trim() ? gameState.currentLevelId : null; - await startPuzzleRunFromProfile(profileId, 'platform', undefined, false, levelId); + await startPuzzleRunFromProfile( + profileId, + 'platform', + undefined, + false, + levelId, + ); } catch (error) { platformBootstrap.setSaveError( resolvePuzzleErrorMessage(error, '恢复拼图存档失败。'), @@ -2191,7 +2246,27 @@ export function PlatformEntryFlowShellImpl({ if (isLocalPuzzleRun(puzzleRun)) { setPuzzleRun(submitLocalPuzzleLeaderboard(puzzleRun, payload.nickname)); - setIsPuzzleLeaderboardBusy(false); + void advanceLocalPuzzleNextLevel({ + run: puzzleRun, + sourceSessionId: + selectedPuzzleDetail?.sourceSessionId ?? + puzzleSession?.sessionId ?? + null, + }) + .then(({ run }) => { + setPuzzleRun((currentRun) => { + if (!currentRun) { + return currentRun; + } + return mergePuzzleServiceRuntimeState(currentRun, run); + }); + }) + .catch(() => { + // 中文注释:本地试玩缺少后端候选时保留本地排行榜和既有下一关入口,避免结算被探测请求打断。 + }) + .finally(() => { + setIsPuzzleLeaderboardBusy(false); + }); return; } @@ -2203,6 +2278,7 @@ export function PlatformEntryFlowShellImpl({ } return mergePuzzleServiceRuntimeState(currentRun, run); }); + void platformBootstrap.refreshSaveArchives(); }) .catch((error) => { submittedPuzzleLeaderboardKeysRef.current.delete(submitKey); @@ -2215,8 +2291,11 @@ export function PlatformEntryFlowShellImpl({ }); }, [ authUi?.user?.displayName, + platformBootstrap, puzzleRun, + puzzleSession, resolvePuzzleErrorMessage, + selectedPuzzleDetail, setPuzzleError, ]); @@ -2263,6 +2342,9 @@ export function PlatformEntryFlowShellImpl({ }) : await advancePuzzleNextLevel(puzzleRun.runId); setPuzzleRun(run); + if (!isLocalPuzzleRun(puzzleRun)) { + void platformBootstrap.refreshSaveArchives(); + } } catch (error) { setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。')); } finally { @@ -2272,6 +2354,7 @@ export function PlatformEntryFlowShellImpl({ }, [ isPuzzleBusy, isPuzzleLeaderboardBusy, + platformBootstrap, puzzleRun, puzzleSession, resolvePuzzleErrorMessage, @@ -2565,6 +2648,55 @@ export function PlatformEntryFlowShellImpl({ [], ); + const handleClaimPuzzlePointIncentive = useCallback( + (work: PuzzleWorkSummary) => { + if (claimingPuzzlePointIncentiveProfileId) { + return; + } + + runProtectedAction(() => { + setClaimingPuzzlePointIncentiveProfileId(work.profileId); + setPuzzleError(null); + + void claimPuzzleWorkPointIncentive(work.profileId) + .then((response) => { + const updatedWork = response.item; + setPuzzleWorks((current) => + current.map((item) => + mergePuzzleWorkSummary(item, updatedWork), + ), + ); + setPuzzleGalleryEntries((current) => + current.map((item) => + mergePuzzleWorkSummary(item, updatedWork), + ), + ); + setSelectedPuzzleDetail((current) => + current ? mergePuzzleWorkSummary(current, updatedWork) : current, + ); + syncUpdatedPublicWorkDetail( + mapPuzzleWorkToPublicWorkDetail(updatedWork), + ); + }) + .catch((error) => { + setPuzzleError( + resolvePuzzleErrorMessage(error, '领取拼图积分激励失败。'), + ); + }) + .finally(() => { + setClaimingPuzzlePointIncentiveProfileId(null); + }); + }); + }, + [ + claimingPuzzlePointIncentiveProfileId, + resolvePuzzleErrorMessage, + runProtectedAction, + setPuzzleError, + syncUpdatedPublicWorkDetail, + ], + ); + const likePublicWork = useCallback( (entry: PlatformPublicGalleryCard) => { if (isPublicWorkDetailBusy) { @@ -3480,6 +3612,10 @@ export function PlatformEntryFlowShellImpl({ onDeletePuzzle={(item) => { handleDeletePuzzleWork(item); }} + onClaimPuzzlePointIncentive={(item) => { + handleClaimPuzzlePointIncentive(item); + }} + claimingPuzzleProfileId={claimingPuzzlePointIncentiveProfileId} /> ); diff --git a/src/components/puzzle-result/PuzzleResultView.test.tsx b/src/components/puzzle-result/PuzzleResultView.test.tsx index af1af2be..5c4b5d49 100644 --- a/src/components/puzzle-result/PuzzleResultView.test.tsx +++ b/src/components/puzzle-result/PuzzleResultView.test.tsx @@ -156,6 +156,7 @@ describe('PuzzleResultView', () => { expect(screen.getByRole('button', { name: '拼图关卡' })).toBeTruthy(); expect(screen.getByRole('button', { name: '作品信息' })).toBeTruthy(); expect(screen.getByText('雨夜猫街')).toBeTruthy(); + expect(screen.getByText('获得更多积分激励')).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: '作品信息' })); expect(screen.getByLabelText('作品名称')).toHaveProperty( diff --git a/src/components/puzzle-result/PuzzleResultView.tsx b/src/components/puzzle-result/PuzzleResultView.tsx index e720cf5d..741fd40a 100644 --- a/src/components/puzzle-result/PuzzleResultView.tsx +++ b/src/components/puzzle-result/PuzzleResultView.tsx @@ -1025,6 +1025,9 @@ function PuzzleLevelListTab({ 新增关卡 + + 获得更多积分激励 +
); diff --git a/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx b/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx index 4d7e32bc..eea8a77a 100644 --- a/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx +++ b/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx @@ -88,6 +88,7 @@ const clearedRun: PuzzleRunSnapshot = { currentLevel: { runId: 'run-1', levelIndex: 1, + levelId: 'puzzle-level-1', gridSize: 3, profileId: 'profile-1', levelName: '潮雾拼图', @@ -307,6 +308,48 @@ test('当前作品没有下一关时展示三个相似作品并可选择进入', vi.useRealTimers(); }); +test('当前作品没有下一关时底部入口打开相似作品选择', () => { + vi.useFakeTimers(); + const similarWorksRun: PuzzleRunSnapshot = { + ...clearedRun, + recommendedNextProfileId: 'profile-similar-1', + nextLevelMode: 'similarWorks', + nextLevelProfileId: 'profile-similar-1', + nextLevelId: null, + recommendedNextWorks: [ + { + profileId: 'profile-similar-1', + levelName: '雾海遗迹', + authorDisplayName: '星桥旅人', + themeTags: ['奇幻', '遗迹'], + coverImageSrc: null, + similarityScore: 0.91, + }, + ], + }; + + renderPuzzleRuntime( + , + ); + + act(() => { + vi.advanceTimersByTime(1_400); + }); + fireEvent.click(screen.getByRole('button', { name: '关闭通关弹窗' })); + + fireEvent.click(screen.getByRole('button', { name: /换个作品/u })); + + expect(screen.getByRole('dialog', { name: '通关完成' })).toBeTruthy(); + expect(screen.getByRole('button', { name: /雾海遗迹/u })).toBeTruthy(); + vi.useRealTimers(); +}); + test('右上角设置按钮打开拼图设置并支持音量调节', () => { const authValue = createAuthValue(); @@ -679,6 +722,95 @@ test('倒计时归零时通知父层同步失败态', () => { vi.useRealTimers(); }); +test('失败弹窗支持重开当前关和续时确认', async () => { + const onRestartLevel = vi.fn(); + const onUseProp = vi.fn().mockResolvedValue({ + ...clearedRun, + currentLevel: { + ...clearedRun.currentLevel!, + status: 'playing', + remainingMs: 60_000, + }, + }); + const failedRun: PuzzleRunSnapshot = { + ...clearedRun, + currentLevel: { + ...clearedRun.currentLevel!, + status: 'failed', + elapsedMs: 180_000, + remainingMs: 0, + board: { + ...clearedRun.currentLevel!.board, + allTilesResolved: false, + }, + }, + }; + + renderPuzzleRuntime( + , + ); + + const failedDialog = screen.getByRole('dialog', { name: '关卡失败' }); + fireEvent.click(within(failedDialog).getByRole('button', { name: '重新开始' })); + expect(onRestartLevel).toHaveBeenCalledTimes(1); + + fireEvent.click( + within(failedDialog).getByRole('button', { name: '继续1分钟' }), + ); + expect(screen.getByRole('dialog', { name: '继续1分钟' })).toBeTruthy(); + expect(screen.getByText('消耗 1 陶泥币')).toBeTruthy(); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: '确定' })); + }); + + expect(onUseProp).toHaveBeenCalledWith('extendTime'); +}); + +test('失败续时扣费失败时保留确认弹窗', async () => { + const onUseProp = vi.fn().mockRejectedValue(new Error('陶泥币余额不足')); + const failedRun: PuzzleRunSnapshot = { + ...clearedRun, + currentLevel: { + ...clearedRun.currentLevel!, + status: 'failed', + elapsedMs: 180_000, + remainingMs: 0, + board: { + ...clearedRun.currentLevel!.board, + allTilesResolved: false, + }, + }, + }; + + renderPuzzleRuntime( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '继续1分钟' })); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: '确定' })); + }); + + expect(screen.getByRole('dialog', { name: '继续1分钟' })).toBeTruthy(); + expect(screen.getByText('陶泥币余额不足')).toBeTruthy(); +}); + test('查看原图开关打开覆盖层并在关闭后恢复计时', async () => { const onPauseChange = vi.fn(); const onUseProp = vi.fn().mockResolvedValue(clearedRun); diff --git a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx index 0f0bf615..6ed5a2ac 100644 --- a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx +++ b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx @@ -756,7 +756,7 @@ export function PuzzleRuntimeShell({ }, [currentLevel?.levelIndex, currentLevel?.runId, currentLevel?.status]); useEffect(() => { - if (!run || !currentLevel || currentLevel.status !== 'playing') { + if (!run || !currentLevel || currentLevel.status === 'cleared') { return; } if (displayRemainingMs > 0) { diff --git a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx index 365978fe..fc1a75ba 100644 --- a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx +++ b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx @@ -506,6 +506,7 @@ function buildMockPuzzleRun( currentLevel: { runId: `run-${profileId}`, levelIndex: 1, + levelId: 'puzzle-level-1', gridSize, profileId, levelName, diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index 7bfd617c..0830978f 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -505,6 +505,18 @@ test('profile total play time card always uses hours', () => { expect(within(playTimeCard).queryByText('90分')).toBeNull(); }); +test('profile played works card shows count unit', () => { + renderProfileView(vi.fn(), { + playedWorldCount: 1, + }); + + const playedCard = screen.getByRole('button', { + name: /玩过\s*1个/u, + }); + + expect(within(playedCard).getByText('1个')).toBeTruthy(); +}); + test('desktop account entry uses saved avatar image when available', () => { mockDesktopLayout(); const avatarUrl = 'data:image/png;base64,AAAA'; diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 64b37a55..01e6cc90 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -3235,7 +3235,7 @@ export function RpgEntryHomeView({ diff --git a/src/components/rpg-entry/useRpgEntryBootstrap.ts b/src/components/rpg-entry/useRpgEntryBootstrap.ts index 0f3d7a30..fb7f17fd 100644 --- a/src/components/rpg-entry/useRpgEntryBootstrap.ts +++ b/src/components/rpg-entry/useRpgEntryBootstrap.ts @@ -136,6 +136,25 @@ export function useRpgEntryBootstrap( return nextEntries; }, [canReadProtectedData, user]); + const refreshSaveArchives = useCallback(async () => { + if (!user || !canReadProtectedData) { + setSaveEntries([]); + setSaveError(null); + return []; + } + + setSaveError(null); + + try { + const nextEntries = await listRpgProfileSaveArchives(); + setSaveEntries(nextEntries); + return nextEntries; + } catch (error) { + setSaveError(resolveRpgEntryErrorMessage(error, '读取存档列表失败。')); + return []; + } + }, [canReadProtectedData, user]); + const appendBrowseHistoryEntry = useCallback( async (entry: PlatformBrowseHistoryWriteEntry) => { setHistoryError(null); @@ -371,6 +390,7 @@ export function useRpgEntryBootstrap( refreshCustomWorldWorks, refreshPublishedGallery, refreshSavedCustomWorldLibrary, + refreshSaveArchives, appendBrowseHistoryEntry, handleResumeSaveEntry, }; diff --git a/src/index.css b/src/index.css index 4d74bfab..06b1496d 100644 --- a/src/index.css +++ b/src/index.css @@ -1450,6 +1450,77 @@ body { line-height: 1; } +.creation-work-card-incentive { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; + align-items: stretch; + gap: 0.38rem; + min-width: 0; +} + +.creation-work-card-incentive__metric, +.creation-work-card-incentive__button { + min-width: 0; + border: 1px solid color-mix(in srgb, #6b7cff 22%, transparent); + border-radius: 0.5rem; + background: color-mix(in srgb, var(--platform-neutral-bg) 82%, transparent); + backdrop-filter: blur(14px); +} + +.creation-work-card-incentive__metric { + overflow: hidden; + padding: 0.38rem 0.42rem; +} + +.creation-work-card-incentive__label { + display: block; + overflow: hidden; + color: var(--platform-text-soft); + font-size: 0.58rem; + font-weight: 800; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.creation-work-card-incentive__value { + display: block; + margin-top: 0.18rem; + overflow: hidden; + color: var(--platform-text-strong); + font-size: 0.95rem; + font-weight: 950; + line-height: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.creation-work-card-incentive__button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.55rem; + padding: 0 0.58rem; + color: var(--platform-text-strong); + font-size: 0.68rem; + font-weight: 900; + line-height: 1.1; + transition: + transform 180ms ease, + border-color 180ms ease, + opacity 180ms ease; +} + +.creation-work-card-incentive__button:hover:not(:disabled) { + transform: translateY(-1px); + border-color: color-mix(in srgb, #6b7cff 44%, transparent); +} + +.creation-work-card-incentive__button:disabled { + cursor: not-allowed; + opacity: 0.52; +} + .platform-tab { border: 1px solid var(--platform-subpanel-border); border-radius: 9999px; @@ -1864,6 +1935,28 @@ body { font-size: 0.54rem; } + .creation-work-card-incentive { + gap: 0.28rem; + } + + .creation-work-card-incentive__metric { + padding: 0.34rem 0.34rem; + } + + .creation-work-card-incentive__label { + font-size: 0.54rem; + } + + .creation-work-card-incentive__value { + font-size: 0.82rem; + } + + .creation-work-card-incentive__button { + min-height: 2.34rem; + padding: 0 0.48rem; + font-size: 0.62rem; + } + .platform-tab-panel { padding-right: 0; } diff --git a/src/services/puzzle-runtime/puzzleLocalRuntime.test.ts b/src/services/puzzle-runtime/puzzleLocalRuntime.test.ts index a1e4b393..b45404dc 100644 --- a/src/services/puzzle-runtime/puzzleLocalRuntime.test.ts +++ b/src/services/puzzle-runtime/puzzleLocalRuntime.test.ts @@ -6,8 +6,11 @@ import { applyLocalPuzzleFreezeTime, advanceLocalPuzzleLevel, dragLocalPuzzlePiece, + extendLocalPuzzleTime, isLocalPuzzleRun, refreshLocalPuzzleTimer, + restartLocalPuzzleLevel, + resolvePuzzleRestartLevelId, setLocalPuzzlePaused, startLocalPuzzleRun, submitLocalPuzzleLeaderboard, @@ -84,6 +87,50 @@ function solveCurrentLevel(run: ReturnType) { } describe('puzzleLocalRuntime', () => { + test('本地关卡切割和倒计时按正式配置推进并循环', () => { + let run = startLocalPuzzleRun(baseWork); + const actual = [run]; + for (let index = 0; index < 15; index += 1) { + run = advanceLocalPuzzleLevel({ + ...run, + clearedLevelCount: run.clearedLevelCount + 1, + currentLevel: run.currentLevel + ? { + ...run.currentLevel, + status: 'cleared', + clearedAtMs: Date.now(), + elapsedMs: 1_000, + } + : null, + }); + actual.push(run); + } + + expect( + actual.map((item) => [ + item.currentLevel?.gridSize, + item.currentLevel?.timeLimitMs, + ]), + ).toEqual([ + [3, 300_000], + [4, 300_000], + [5, 300_000], + [5, 210_000], + [5, 210_000], + [6, 240_000], + [5, 210_000], + [7, 270_000], + [5, 240_000], + [7, 270_000], + [5, 210_000], + [6, 240_000], + [5, 210_000], + [7, 270_000], + [5, 240_000], + [7, 270_000], + ]); + }); + test('每次启动都会生成不同的初始打乱样式', async () => { const firstRun = startLocalPuzzleRun(baseWork); await new Promise((resolve) => setTimeout(resolve, 2)); @@ -297,6 +344,8 @@ describe('puzzleLocalRuntime', () => { expect(clearedRun.currentLevel?.status).toBe('cleared'); expect(clearedRun.recommendedNextProfileId).toBeNull(); + expect(clearedRun.nextLevelMode).toBe('none'); + expect(clearedRun.recommendedNextWorks).toEqual([]); expect(clearedRun.currentLevel?.elapsedMs).toBeGreaterThan(0); expect(clearedRun.currentLevel?.leaderboardEntries).toEqual([]); expect(clearedRun.leaderboardEntries).toEqual([]); @@ -310,6 +359,8 @@ describe('puzzleLocalRuntime', () => { expect(nextRun.currentLevel?.elapsedMs).toBeNull(); expect(nextRun.currentLevel?.leaderboardEntries).toEqual([]); expect(nextRun.recommendedNextProfileId).toBeNull(); + expect(nextRun.nextLevelMode).toBe('none'); + expect(nextRun.recommendedNextWorks).toEqual([]); }); test('连续推进下一关会重新打乱棋盘', () => { @@ -376,6 +427,113 @@ describe('puzzleLocalRuntime', () => { expect(nextRun).toBe(timedRun); }); + test('本地失败关卡可以续时一分钟', () => { + const run = startLocalPuzzleRun(baseWork); + const failedRun = refreshLocalPuzzleTimer({ + ...run, + currentLevel: run.currentLevel + ? { + ...run.currentLevel, + startedAtMs: Date.now() - run.currentLevel.timeLimitMs - 1_000, + } + : null, + }); + + const extendedRun = extendLocalPuzzleTime(failedRun); + + expect(extendedRun.currentLevel?.status).toBe('playing'); + expect(extendedRun.currentLevel?.remainingMs).toBe(60_000); + expect(extendedRun.currentLevel?.elapsedMs).toBeNull(); + expect(extendedRun.currentLevel?.pauseStartedAtMs).toBeNull(); + expect(extendedRun.currentLevel?.freezeUntilMs).toBeNull(); + }); + + test('本地失败关卡重新开始会保留关卡索引并重建棋盘', () => { + const run = startLocalPuzzleRun(baseWork); + const failedRun = refreshLocalPuzzleTimer({ + ...run, + currentLevel: run.currentLevel + ? { + ...run.currentLevel, + startedAtMs: Date.now() - run.currentLevel.timeLimitMs - 1_000, + } + : null, + }); + + const restartedRun = restartLocalPuzzleLevel(failedRun); + + expect(restartedRun.runId).not.toBe(failedRun.runId); + expect(restartedRun.currentLevel?.status).toBe('playing'); + expect(restartedRun.currentLevel?.levelIndex).toBe( + failedRun.currentLevel?.levelIndex, + ); + expect(restartedRun.currentLevel?.remainingMs).toBe( + restartedRun.currentLevel?.timeLimitMs, + ); + expect(boardPositionSignature(restartedRun)).not.toBe( + boardPositionSignature(failedRun), + ); + }); + + test('失败重开优先使用当前关卡 id,旧快照缺失时按关卡序号兜底', () => { + const workWithLevels: PuzzleWorkSummary = { + ...baseWork, + levels: [ + { + levelId: 'puzzle-level-1', + levelName: '第一关', + pictureDescription: '第一关画面', + candidates: [], + selectedCandidateId: null, + coverImageSrc: '/level-1.png', + coverAssetId: null, + generationStatus: 'ready', + }, + { + levelId: 'puzzle-level-2', + levelName: '第二关', + pictureDescription: '第二关画面', + candidates: [], + selectedCandidateId: null, + coverImageSrc: '/level-2.png', + coverAssetId: null, + generationStatus: 'ready', + }, + ], + }; + const run = startLocalPuzzleRun(workWithLevels); + const secondLevelRun = { + ...run, + currentLevelIndex: 2, + currentLevel: run.currentLevel + ? { + ...run.currentLevel, + levelIndex: 2, + levelId: null, + status: 'failed' as const, + } + : null, + }; + + expect(resolvePuzzleRestartLevelId(secondLevelRun, workWithLevels)).toBe( + 'puzzle-level-2', + ); + expect( + resolvePuzzleRestartLevelId( + { + ...secondLevelRun, + currentLevel: secondLevelRun.currentLevel + ? { + ...secondLevelRun.currentLevel, + levelId: 'explicit-level', + } + : null, + }, + workWithLevels, + ), + ).toBe('explicit-level'); + }); + test('暂停和冻结时间不会消耗本地倒计时', () => { const run = startLocalPuzzleRun(baseWork); const pausedRun = setLocalPuzzlePaused( diff --git a/src/services/puzzle-runtime/puzzleLocalRuntime.ts b/src/services/puzzle-runtime/puzzleLocalRuntime.ts index 8bca027a..6a4d4aef 100644 --- a/src/services/puzzle-runtime/puzzleLocalRuntime.ts +++ b/src/services/puzzle-runtime/puzzleLocalRuntime.ts @@ -15,17 +15,70 @@ import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/p const LOCAL_PUZZLE_RUN_ID_PREFIX = 'local-puzzle-run-'; const PUZZLE_FREEZE_TIME_DURATION_MS = 10_000; const PUZZLE_EXTEND_TIME_DURATION_MS = 60_000; -const PUZZLE_LEVEL_TIME_LIMIT_MS_BY_GRID_SIZE: Record = { - 3: 180_000, - 4: 300_000, +let localPuzzleRunSequence = 0; +type PuzzleLevelConfig = { + gridSize: PuzzleGridSize; + timeLimitMs: number; }; +// 中文注释:本地兜底必须和后端按同一关卡序号解析切割规格与倒计时。 +function resolvePuzzleLevelConfig(levelIndex: number): PuzzleLevelConfig { + const normalizedLevelIndex = Math.max(1, Math.floor(levelIndex || 1)); + switch (normalizedLevelIndex) { + case 1: + return { gridSize: 3, timeLimitMs: 300_000 }; + case 2: + return { gridSize: 4, timeLimitMs: 300_000 }; + case 3: + return { gridSize: 5, timeLimitMs: 300_000 }; + case 4: + return { gridSize: 5, timeLimitMs: 210_000 }; + default: { + const loopIndex = ((Math.max(5, normalizedLevelIndex) - 5) % 6) + 5; + switch (loopIndex) { + case 5: + return { gridSize: 5, timeLimitMs: 210_000 }; + case 6: + return { gridSize: 6, timeLimitMs: 240_000 }; + case 7: + return { gridSize: 5, timeLimitMs: 210_000 }; + case 8: + return { gridSize: 7, timeLimitMs: 270_000 }; + case 9: + return { gridSize: 5, timeLimitMs: 240_000 }; + default: + return { gridSize: 7, timeLimitMs: 270_000 }; + } + } + } +} + function resolvePuzzleGridSize(clearedLevelCount: number): PuzzleGridSize { - return clearedLevelCount >= 3 ? 4 : 3; + return resolvePuzzleLevelConfig(clearedLevelCount + 1).gridSize; } const PUZZLE_INITIAL_SHUFFLE_ATTEMPTS = 64; +function buildLocalPuzzleRunId(profileId: string) { + localPuzzleRunSequence = (localPuzzleRunSequence + 1) % 1_000_000; + return `${LOCAL_PUZZLE_RUN_ID_PREFIX}${profileId}-${Date.now()}-${localPuzzleRunSequence}`; +} + +export function resolvePuzzleRestartLevelId( + run: PuzzleRunSnapshot, + work: PuzzleWorkSummary | null | undefined, +): string | null { + const currentLevel = run.currentLevel; + if (!currentLevel) { + return null; + } + return ( + currentLevel.levelId ?? + work?.levels?.[Math.max(0, currentLevel.levelIndex - 1)]?.levelId ?? + null + ); +} + function buildShuffleSeed(...parts: Array) { let hash = 0x811c9dc5; for (const part of parts.join('|')) { @@ -77,7 +130,11 @@ function buildInitialPositions(gridSize: PuzzleGridSize, seed: number) { row: Math.floor(index / gridSize), col: index % gridSize, })); - for (let attempt = 0; attempt < PUZZLE_INITIAL_SHUFFLE_ATTEMPTS; attempt += 1) { + for ( + let attempt = 0; + attempt < PUZZLE_INITIAL_SHUFFLE_ATTEMPTS; + attempt += 1 + ) { const shuffled = shufflePositions( positions, (seed + Math.imul(attempt, 2654435761)) >>> 0, @@ -88,7 +145,11 @@ function buildInitialPositions(gridSize: PuzzleGridSize, seed: number) { return shuffled; } } - return buildOriginalNeighborFreePositions(gridSize, seed) ?? positions; + return ( + buildDeterministicNeighborFreePositions(gridSize, seed) ?? + buildOriginalNeighborFreePositions(gridSize, seed) ?? + positions + ); } function boardCellKey(row: number, col: number) { @@ -99,8 +160,8 @@ function clampElapsedMs(value: number) { return Math.max(1_000, Math.round(value)); } -function resolvePuzzleLevelTimeLimitMs(gridSize: PuzzleGridSize) { - return PUZZLE_LEVEL_TIME_LIMIT_MS_BY_GRID_SIZE[gridSize]; +function resolvePuzzleLevelTimeLimitMs(levelIndex: number) { + return resolvePuzzleLevelConfig(levelIndex || 1).timeLimitMs; } function resolveActiveFreezeElapsedMs( @@ -110,7 +171,10 @@ function resolveActiveFreezeElapsedMs( if (!level.freezeStartedAtMs || !level.freezeUntilMs) { return 0; } - return Math.max(0, Math.min(nowMs, level.freezeUntilMs) - level.freezeStartedAtMs); + return Math.max( + 0, + Math.min(nowMs, level.freezeUntilMs) - level.freezeStartedAtMs, + ); } function resolveEffectiveElapsedMs( @@ -135,7 +199,11 @@ function settleExpiredFreeze( level: PuzzleRuntimeLevelSnapshot, nowMs: number, ): PuzzleRuntimeLevelSnapshot { - if (!level.freezeStartedAtMs || !level.freezeUntilMs || nowMs < level.freezeUntilMs) { + if ( + !level.freezeStartedAtMs || + !level.freezeUntilMs || + nowMs < level.freezeUntilMs + ) { return level; } return { @@ -168,8 +236,8 @@ function withResolvedTimer(run: PuzzleRunSnapshot, nowMs = Date.now()) { }; } -function buildLevelTimerFields(gridSize: PuzzleGridSize) { - const timeLimitMs = resolvePuzzleLevelTimeLimitMs(gridSize); +function buildLevelTimerFields(levelIndex: number) { + const timeLimitMs = resolvePuzzleLevelTimeLimitMs(levelIndex); return { timeLimitMs, remainingMs: timeLimitMs, @@ -228,18 +296,6 @@ function buildPiecesFromPositions( })); } -function hasAnyCorrectNeighborPair(pieces: PuzzlePieceState[]) { - const piecesByCell = new Map( - pieces.map((piece) => [boardCellKey(piece.currentRow, piece.currentCol), piece]), - ); - return pieces.some((piece) => - neighborCells(piece.currentRow, piece.currentCol).some((neighbor) => { - const neighborPiece = piecesByCell.get(boardCellKey(neighbor.row, neighbor.col)); - return Boolean(neighborPiece && areCorrectNeighbors(piece, neighborPiece)); - }), - ); -} - function areOriginalNeighbors(left: PuzzlePieceState, right: PuzzlePieceState) { return ( Math.abs(right.correctRow - left.correctRow) + @@ -250,12 +306,19 @@ function areOriginalNeighbors(left: PuzzlePieceState, right: PuzzlePieceState) { function hasAnyOriginalNeighborPair(pieces: PuzzlePieceState[]) { const piecesByCell = new Map( - pieces.map((piece) => [boardCellKey(piece.currentRow, piece.currentCol), piece]), + pieces.map((piece) => [ + boardCellKey(piece.currentRow, piece.currentCol), + piece, + ]), ); return pieces.some((piece) => neighborCells(piece.currentRow, piece.currentCol).some((neighbor) => { - const neighborPiece = piecesByCell.get(boardCellKey(neighbor.row, neighbor.col)); - return Boolean(neighborPiece && areOriginalNeighbors(piece, neighborPiece)); + const neighborPiece = piecesByCell.get( + boardCellKey(neighbor.row, neighbor.col), + ); + return Boolean( + neighborPiece && areOriginalNeighbors(piece, neighborPiece), + ); }), ); } @@ -269,6 +332,127 @@ function seededOrderKey(seed: number, value: number) { return (state ^ (state >>> 16)) >>> 0; } +function buildDeterministicNeighborFreePositions( + gridSize: PuzzleGridSize, + seed: number, +) { + if (gridSize === 3) { + return buildSeeded3x3NeighborFreePositions(seed); + } + if (gridSize === 4 || gridSize === 6) { + return buildAffineNeighborFreePositions(gridSize, 1, 1, 2, 1, seed); + } + if (gridSize === 5 || gridSize === 7) { + return buildAffineNeighborFreePositions( + gridSize, + 0, + 1, + 2, + gridSize - 1, + seed, + ); + } + return null; +} + +function buildSeeded3x3NeighborFreePositions(seed: number) { + const layouts: Array> = [ + [ + [0, 1], + [1, 0], + [1, 2], + [2, 0], + [0, 2], + [2, 1], + [1, 1], + [2, 2], + [0, 0], + ], + [ + [0, 1], + [1, 0], + [1, 2], + [2, 0], + [0, 2], + [2, 1], + [2, 2], + [1, 1], + [0, 0], + ], + [ + [0, 1], + [1, 0], + [1, 2], + [2, 0], + [2, 2], + [0, 0], + [1, 1], + [0, 2], + [2, 1], + ], + [ + [0, 1], + [1, 0], + [1, 2], + [2, 1], + [0, 2], + [2, 0], + [0, 0], + [2, 2], + [1, 1], + ], + [ + [0, 1], + [1, 0], + [1, 2], + [2, 2], + [0, 2], + [2, 1], + [1, 1], + [2, 0], + [0, 0], + ], + [ + [0, 1], + [1, 0], + [2, 1], + [2, 0], + [2, 2], + [0, 2], + [1, 2], + [0, 0], + [1, 1], + ], + ]; + const layout = layouts[Math.abs(seed) % layouts.length] ?? layouts[0]; + return ( + layout?.map(([row, col]) => ({ + row, + col, + })) ?? null + ); +} + +function buildAffineNeighborFreePositions( + gridSize: PuzzleGridSize, + rowFromRow: number, + rowFromCol: number, + colFromRow: number, + colFromCol: number, + seed: number, +) { + const rowOffset = seed % gridSize; + const colOffset = Math.floor(seed / gridSize) % gridSize; + return Array.from({ length: gridSize * gridSize }, (_, index) => { + const row = Math.floor(index / gridSize); + const col = index % gridSize; + return { + row: (rowFromRow * row + rowFromCol * col + rowOffset) % gridSize, + col: (colFromRow * row + colFromCol * col + colOffset) % gridSize, + }; + }); +} + function buildOriginalNeighborFreePositions( gridSize: PuzzleGridSize, seed: number, @@ -343,11 +527,14 @@ function violatesOriginalNeighborFreeRule( return false; } const originalNeighbors = - Math.abs(Math.floor(pieceIndex / gridSize) - Math.floor(placedIndex / gridSize)) + + Math.abs( + Math.floor(pieceIndex / gridSize) - Math.floor(placedIndex / gridSize), + ) + Math.abs((pieceIndex % gridSize) - (placedIndex % gridSize)) === 1; const currentNeighbors = - Math.abs(cell.row - placedCell.row) + Math.abs(cell.col - placedCell.col) === + Math.abs(cell.row - placedCell.row) + + Math.abs(cell.col - placedCell.col) === 1; return originalNeighbors && currentNeighbors; }); @@ -357,7 +544,10 @@ function resolveMergedGroups( pieces: PuzzlePieceState[], ): PuzzleMergedGroupState[] { const piecesByCell = new Map( - pieces.map((piece) => [boardCellKey(piece.currentRow, piece.currentCol), piece]), + pieces.map((piece) => [ + boardCellKey(piece.currentRow, piece.currentCol), + piece, + ]), ); const piecesById = new Map(pieces.map((piece) => [piece.pieceId, piece])); const visited = new Set(); @@ -386,7 +576,9 @@ function resolveMergedGroups( currentPiece.currentRow, currentPiece.currentCol, )) { - const neighborPiece = piecesByCell.get(boardCellKey(neighbor.row, neighbor.col)); + const neighborPiece = piecesByCell.get( + boardCellKey(neighbor.row, neighbor.col), + ); if (neighborPiece && areCorrectNeighbors(currentPiece, neighborPiece)) { queue.push(neighborPiece.pieceId); } @@ -433,7 +625,8 @@ function rebuildBoardSnapshot( piece.currentCol === piece.correctCol, ); const allPiecesMergedIntoOneGroup = mergedGroups.some( - (group) => group.pieceIds.length === nextPieces.length && nextPieces.length > 1, + (group) => + group.pieceIds.length === nextPieces.length && nextPieces.length > 1, ); const allTilesResolved = allPiecesInCorrectCells || allPiecesMergedIntoOneGroup; @@ -560,12 +753,17 @@ function buildFallbackLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot { gridSize, profileId: nextProfileId, levelName: buildLocalLevelName(currentLevel.levelName, nextLevelIndex), - board: buildInitialBoard(gridSize, run.runId, nextProfileId, nextLevelIndex), + board: buildInitialBoard( + gridSize, + run.runId, + nextProfileId, + nextLevelIndex, + ), status: 'playing', startedAtMs, clearedAtMs: null, elapsedMs: null, - ...buildLevelTimerFields(gridSize), + ...buildLevelTimerFields(nextLevelIndex), leaderboardEntries: [], }, recommendedNextProfileId: null, @@ -577,9 +775,11 @@ function buildFallbackLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot { }; } -export function startLocalPuzzleRun(item: PuzzleWorkSummary): PuzzleRunSnapshot { +export function startLocalPuzzleRun( + item: PuzzleWorkSummary, +): PuzzleRunSnapshot { const gridSize = resolvePuzzleGridSize(0); - const runId = `${LOCAL_PUZZLE_RUN_ID_PREFIX}${item.profileId}-${Date.now()}`; + const runId = buildLocalPuzzleRunId(item.profileId); const startedAtMs = Date.now(); const firstLevel = item.levels?.[0] ?? null; const firstLevelName = firstLevel?.levelName || item.levelName; @@ -608,7 +808,7 @@ export function startLocalPuzzleRun(item: PuzzleWorkSummary): PuzzleRunSnapshot startedAtMs, clearedAtMs: null, elapsedMs: null, - ...buildLevelTimerFields(gridSize), + ...buildLevelTimerFields(1), leaderboardEntries: [], }, recommendedNextProfileId: null, @@ -631,7 +831,9 @@ export function swapLocalPuzzlePieces( } const pieces = currentLevel.board.pieces.map((piece) => ({ ...piece })); const first = pieces.find((piece) => piece.pieceId === payload.firstPieceId); - const second = pieces.find((piece) => piece.pieceId === payload.secondPieceId); + const second = pieces.find( + (piece) => piece.pieceId === payload.secondPieceId, + ); if (!first || !second) { return timedRun; } @@ -641,7 +843,10 @@ export function swapLocalPuzzlePieces( second.currentRow = firstPosition.row; second.currentCol = firstPosition.col; - return applyNextBoard(timedRun, rebuildBoardSnapshot(currentLevel.gridSize, pieces)); + return applyNextBoard( + timedRun, + rebuildBoardSnapshot(currentLevel.gridSize, pieces), + ); } function dragSinglePiece( @@ -717,7 +922,8 @@ function dragGroup( col: piece.currentCol, })) .filter( - (position) => !targetCellKeys.has(boardCellKey(position.row, position.col)), + (position) => + !targetCellKeys.has(boardCellKey(position.row, position.col)), ) .sort((left, right) => left.row - right.row || left.col - right.col); const occupyingPieces = targetPositions @@ -733,7 +939,8 @@ function dragGroup( .filter((piece): piece is PuzzlePieceState => Boolean(piece)) .sort( (left, right) => - left.currentRow - right.currentRow || left.currentCol - right.currentCol, + left.currentRow - right.currentRow || + left.currentCol - right.currentCol, ); if (occupyingPieces.length !== vacatedPositions.length) { @@ -796,20 +1003,27 @@ export function dragLocalPuzzlePiece( dragSinglePiece(pieces, moving, payload.targetRow, payload.targetCol); } - return applyNextBoard(timedRun, rebuildBoardSnapshot(currentLevel.gridSize, pieces)); + return applyNextBoard( + timedRun, + rebuildBoardSnapshot(currentLevel.gridSize, pieces), + ); } -export function advanceLocalPuzzleLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot { +export function advanceLocalPuzzleLevel( + run: PuzzleRunSnapshot, +): PuzzleRunSnapshot { return buildFallbackLocalLevel(run); } -export function restartLocalPuzzleLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot { +export function restartLocalPuzzleLevel( + run: PuzzleRunSnapshot, +): PuzzleRunSnapshot { const currentLevel = run.currentLevel; if (!currentLevel) { return run; } - const runId = `${LOCAL_PUZZLE_RUN_ID_PREFIX}${currentLevel.profileId}-${Date.now()}`; + const runId = buildLocalPuzzleRunId(currentLevel.profileId); const startedAtMs = Date.now(); return { ...run, @@ -828,7 +1042,7 @@ export function restartLocalPuzzleLevel(run: PuzzleRunSnapshot): PuzzleRunSnapsh startedAtMs, clearedAtMs: null, elapsedMs: null, - ...buildLevelTimerFields(currentLevel.gridSize), + ...buildLevelTimerFields(currentLevel.levelIndex), leaderboardEntries: [], }, }; @@ -876,7 +1090,9 @@ export function submitLocalPuzzleLeaderboard( }; } -export function refreshLocalPuzzleTimer(run: PuzzleRunSnapshot): PuzzleRunSnapshot { +export function refreshLocalPuzzleTimer( + run: PuzzleRunSnapshot, +): PuzzleRunSnapshot { return withResolvedTimer(run); } @@ -928,7 +1144,9 @@ export function applyLocalPuzzleFreezeTime( }; } -export function extendLocalPuzzleTime(run: PuzzleRunSnapshot): PuzzleRunSnapshot { +export function extendLocalPuzzleTime( + run: PuzzleRunSnapshot, +): PuzzleRunSnapshot { const timedRun = withResolvedTimer(run); const currentLevel = timedRun.currentLevel; if (!currentLevel || currentLevel.status !== 'failed') { diff --git a/src/services/puzzle-works/index.ts b/src/services/puzzle-works/index.ts index 363005c2..cfaa93e0 100644 --- a/src/services/puzzle-works/index.ts +++ b/src/services/puzzle-works/index.ts @@ -1,6 +1,7 @@ export { - getPuzzleWorkDetail, + claimPuzzleWorkPointIncentive, deletePuzzleWork, + getPuzzleWorkDetail, listPuzzleWorks, puzzleWorksClient, updatePuzzleWork, diff --git a/src/services/puzzle-works/puzzleWorksClient.ts b/src/services/puzzle-works/puzzleWorksClient.ts index 3e75dba6..60694c68 100644 --- a/src/services/puzzle-works/puzzleWorksClient.ts +++ b/src/services/puzzle-works/puzzleWorksClient.ts @@ -98,7 +98,24 @@ export async function deletePuzzleWork(profileId: string) { ); } +/** + * 领取当前用户名下拼图作品的整数陶泥币激励。 + */ +export async function claimPuzzleWorkPointIncentive(profileId: string) { + return requestJson( + `${PUZZLE_WORKS_API_BASE}/${encodeURIComponent(profileId)}/point-incentive/claim`, + { + method: 'POST', + }, + '领取拼图积分激励失败', + { + retry: PUZZLE_WORKS_WRITE_RETRY, + }, + ); +} + export const puzzleWorksClient = { + claimPointIncentive: claimPuzzleWorkPointIncentive, delete: deletePuzzleWork, getDetail: getPuzzleWorkDetail, list: listPuzzleWorks,