From 96313dd481f49697b7254bca6a116228081d5cf5 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:53:16 +0800 Subject: [PATCH] 1 --- ...E_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md | 13 +++--- .../PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md | 5 ++- server-rs/crates/module-puzzle/src/lib.rs | 23 +++++++++++ server-rs/crates/spacetime-client/src/lib.rs | 18 ++++----- ...in_upsert_profile_invite_code_procedure.rs | 40 +++++++++---------- ...invite_code_admin_procedure_result_type.rs | 12 ++++-- ...ile_invite_code_admin_upsert_input_type.rs | 9 ++++- ...ntime_profile_invite_code_snapshot_type.rs | 9 ++++- .../crates/spacetime-module/src/puzzle.rs | 21 +++++----- .../PlatformEntryFlowShellImpl.tsx | 5 ++- 10 files changed, 100 insertions(+), 55 deletions(-) diff --git a/docs/technical/PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md b/docs/technical/PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md index ce2b0621..9d963f0f 100644 --- a/docs/technical/PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md +++ b/docs/technical/PUZZLE_FAILURE_EXTENSION_AND_SAVE_ARCHIVE_2026-05-01.md @@ -57,13 +57,13 @@ 1. `world_key = puzzle:{entry_profile_id}`。 2. `world_type = PUZZLE`。 3. `profile_id = entry_profile_id`,保证同一个作品链只覆盖一条存档。 -4. `world_name` 使用当前关卡名。 +4. `world_name` 使用当前可恢复关卡名。 5. `subtitle` 使用 `第 N 关`。 -6. `summary_text` 使用当前状态: +6. `summary_text` 使用可恢复关卡状态: - playing:`拼图进行中` - failed:`关卡失败` - cleared:`关卡已完成` -7. `cover_image_src` 使用当前关卡正式图。 +7. `cover_image_src` 使用可恢复关卡正式图。 8. `game_state_json` 保存最小拼图恢复载荷: - `runtimeKind = "puzzle"` - `runId` @@ -73,6 +73,8 @@ - `currentLevelId` - `status` +通关存档投影有一个额外规则:如果当前关卡已通关,并且 `refresh_next_level_handoff` 已经确认同作品存在下一关,则存档立即投影到同作品下一关入口,`status` 写为 `playing`,`subtitle / world_name / cover_image_src / currentLevelId` 都使用下一关。若当前作品没有下一关、只存在相似作品候选,存档保持当前已通关关卡,等待玩家在结算弹窗里选择相似作品,不能提前替玩家切换到某个候选作品。 + ## 写入时机 SpacetimeDB 拼图运行态每次持久化 run 时同步刷新存档: @@ -81,8 +83,9 @@ SpacetimeDB 拼图运行态每次持久化 run 时同步刷新存档: 2. `advance_puzzle_next_level`:进入下一关后更新同一条存档。 3. `use_puzzle_runtime_prop(extendTime)`:续时成功后更新状态。 4. `get_puzzle_run` 导致失败态落库时,也同步更新为失败存档。 +5. `submit_puzzle_leaderboard_entry`:正式 run 提交成绩并把当前关标记为已通关时,先刷新下一关 handoff,再按上面的通关投影规则同步存档。 -排行榜提交只负责成绩与通关态,不新增存档规则;如果它把 run 状态更新为通关,也跟随 run 持久化刷新存档。 +前端在 `startPuzzleRun / usePuzzleProp / submitPuzzleLeaderboard / advancePuzzleLevel / getPuzzleRun` 成功后主动刷新存档列表,避免存档页停留在进入作品前或上一关的旧投影。 ## 验收 @@ -91,5 +94,5 @@ SpacetimeDB 拼图运行态每次持久化 run 时同步刷新存档: 3. 陶泥币余额不足时确认弹窗保留,并展示错误。 4. 点击 `重新开始` 后当前关卡重新打乱并重置倒计时。 5. 进入拼图作品后,存档页出现 `worldType = PUZZLE` 的拼图存档。 -6. 通过一关进入下一关后,同一条存档更新到新关卡。 +6. 通过一关后,只要后端确认同作品下一关存在,同一条存档立即更新到新关卡;没有同作品下一关时保留已完成关卡,等待玩家选择相似作品。 7. 定向前端测试、Rust 拼图模块测试与编码检查通过。 diff --git a/docs/technical/PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md b/docs/technical/PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md index f22b6a1b..3c753717 100644 --- a/docs/technical/PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md +++ b/docs/technical/PUZZLE_WORK_POINT_INCENTIVE_2026-05-01.md @@ -46,7 +46,7 @@ - `POST /api/runtime/puzzle/works/{profile_id}/point-incentive/claim` - 返回更新后的 `PuzzleWorkProfile`。 3. 创作页仅对已发布拼图作品显示积分激励块;RPG、大鱼和草稿卡不显示。 -4. 领取成功后刷新对应拼图作品列表状态,按钮立即禁用或显示新的待领取数。 +4. 领取成功后刷新对应拼图作品列表状态,按钮立即禁用或显示新的待领取数,并同步刷新个人钱包看板。 5. `spacetime-client` 映射层继续兼容历史拼图运行快照:旧 `run_json` 若缺少 `started_at_ms`,API 记录回填为非 0 值,避免前端计时器拿到无效开始时间。 ## 5. 验收点 @@ -56,4 +56,5 @@ 3. 待领取积分为 0 时领取按钮禁用。 4. 非作者游玩他人拼图并使用付费道具后,该作品累计 half points 增加。 5. 作者领取后钱包增加向下取整后的整数陶泥币,作品待领取数归零或保留不足 1 的小数余额。 -6. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。 +6. 领取成功后顶部/我的页钱包余额随个人看板刷新。 +7. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。 diff --git a/server-rs/crates/module-puzzle/src/lib.rs b/server-rs/crates/module-puzzle/src/lib.rs index e4f0d620..f86e19b5 100644 --- a/server-rs/crates/module-puzzle/src/lib.rs +++ b/server-rs/crates/module-puzzle/src/lib.rs @@ -3459,6 +3459,19 @@ pub fn current_puzzle_unix_micros() -> i64 { (current_unix_ms() as i64).saturating_mul(1_000) } +pub fn puzzle_point_incentive_claimable_points(total_half_points: u64, claimed_points: u64) -> u64 { + total_half_points + .saturating_div(2) + .saturating_sub(claimed_points) +} + +pub fn puzzle_point_incentive_total_after_spend( + total_half_points: u64, + spent_points: u64, +) -> u64 { + total_half_points.saturating_add(spent_points) +} + #[cfg(test)] mod tests { use super::*; @@ -3779,6 +3792,16 @@ mod tests { assert_ne!(first_positions, second_positions); } + #[test] + fn puzzle_point_incentive_uses_half_points_and_floor_claimable() { + // 中文注释:累计单位是 half point,消耗 1 个陶泥币只让作者获得 0.5 个待结算陶泥币。 + assert_eq!(puzzle_point_incentive_total_after_spend(0, 1), 1); + assert_eq!(puzzle_point_incentive_claimable_points(1, 0), 0); + assert_eq!(puzzle_point_incentive_claimable_points(2, 0), 1); + assert_eq!(puzzle_point_incentive_claimable_points(5, 1), 1); + assert_eq!(puzzle_point_incentive_claimable_points(5, 2), 0); + } + #[test] fn initial_board_has_no_original_neighbor_pairs() { for grid_size in PUZZLE_SUPPORTED_GRID_SIZES { diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 104aa88e..2b65e760 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -26,20 +26,20 @@ pub use mapper::{ CustomWorldPublishGateRecord, CustomWorldPublishWorldRecord, CustomWorldPublishWorldRecordInput, CustomWorldPublishedProfileCompileRecord, CustomWorldResultPreviewBlockerRecord, CustomWorldSupportedActionRecord, - CustomWorldWorkSummaryRecord, NpcBattleInteractionRecord, NpcInteractionRecord, NpcStateRecord, + CustomWorldWorkSummaryRecord, Match3DAgentMessageFinalizeRecordInput, + Match3DAgentMessageRecord, Match3DAgentMessageSubmitRecordInput, + Match3DAgentSessionCreateRecordInput, Match3DAgentSessionRecord, Match3DAnchorItemRecord, + Match3DAnchorPackRecord, Match3DClickConfirmationRecord, Match3DCompileDraftRecordInput, + Match3DCreatorConfigRecord, Match3DItemSnapshotRecord, Match3DResultDraftRecord, + Match3DRunClickRecordInput, Match3DRunRecord, Match3DRunRestartRecordInput, + Match3DRunStartRecordInput, Match3DRunStopRecordInput, Match3DRunTimeUpRecordInput, + Match3DTraySlotRecord, Match3DWorkProfileRecord, Match3DWorkUpdateRecordInput, + NpcBattleInteractionRecord, NpcInteractionRecord, NpcStateRecord, PuzzleAgentMessageFinalizeRecordInput, PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput, PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord, PuzzleDraftLevelRecord, PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput, - Match3DAgentMessageFinalizeRecordInput, Match3DAgentMessageRecord, - Match3DAgentMessageSubmitRecordInput, Match3DAgentSessionCreateRecordInput, - Match3DAgentSessionRecord, Match3DAnchorItemRecord, Match3DAnchorPackRecord, - Match3DClickConfirmationRecord, Match3DCompileDraftRecordInput, Match3DCreatorConfigRecord, - Match3DItemSnapshotRecord, Match3DResultDraftRecord, Match3DRunClickRecordInput, - Match3DRunRecord, Match3DRunRestartRecordInput, Match3DRunStartRecordInput, - Match3DRunStopRecordInput, Match3DRunTimeUpRecordInput, Match3DTraySlotRecord, - Match3DWorkProfileRecord, Match3DWorkUpdateRecordInput, PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord, PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_invite_code_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_invite_code_procedure.rs index 3601be97..64105c40 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_invite_code_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_invite_code_procedure.rs @@ -2,17 +2,23 @@ // 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 spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; -use super::runtime_profile_invite_code_admin_procedure_result_type::RuntimeProfileInviteCodeAdminProcedureResult; use super::runtime_profile_invite_code_admin_upsert_input_type::RuntimeProfileInviteCodeAdminUpsertInput; +use super::runtime_profile_invite_code_admin_procedure_result_type::RuntimeProfileInviteCodeAdminProcedureResult; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] -struct AdminUpsertProfileInviteCodeArgs { + struct AdminUpsertProfileInviteCodeArgs { pub input: RuntimeProfileInviteCodeAdminUpsertInput, } + impl __sdk::InModule for AdminUpsertProfileInviteCodeArgs { type Module = super::RemoteModule; } @@ -22,19 +28,16 @@ impl __sdk::InModule for AdminUpsertProfileInviteCodeArgs { /// /// Implemented for [`super::RemoteProcedures`]. pub trait admin_upsert_profile_invite_code { - fn admin_upsert_profile_invite_code(&self, input: RuntimeProfileInviteCodeAdminUpsertInput) { - self.admin_upsert_profile_invite_code_then(input, |_, _| {}); + fn admin_upsert_profile_invite_code(&self, input: RuntimeProfileInviteCodeAdminUpsertInput, +) { + self.admin_upsert_profile_invite_code_then(input, |_, _| {}); } fn admin_upsert_profile_invite_code_then( &self, input: RuntimeProfileInviteCodeAdminUpsertInput, - __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ); } @@ -43,17 +46,12 @@ impl admin_upsert_profile_invite_code for super::RemoteProcedures { &self, input: RuntimeProfileInviteCodeAdminUpsertInput, - __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ) { - self.imp - .invoke_procedure_with_callback::<_, RuntimeProfileInviteCodeAdminProcedureResult>( - "admin_upsert_profile_invite_code", - AdminUpsertProfileInviteCodeArgs { input }, - __callback, - ); + self.imp.invoke_procedure_with_callback::<_, RuntimeProfileInviteCodeAdminProcedureResult>( + "admin_upsert_profile_invite_code", + AdminUpsertProfileInviteCodeArgs { input, }, + __callback, + ); } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_procedure_result_type.rs index 70f54300..afd29a72 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_procedure_result_type.rs @@ -2,7 +2,12 @@ // 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 spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; use super::runtime_profile_invite_code_snapshot_type::RuntimeProfileInviteCodeSnapshot; @@ -10,10 +15,11 @@ use super::runtime_profile_invite_code_snapshot_type::RuntimeProfileInviteCodeSn #[sats(crate = __lib)] pub struct RuntimeProfileInviteCodeAdminProcedureResult { pub ok: bool, - pub record: Option, - pub error_message: Option, + pub record: Option::, + pub error_message: Option::, } + impl __sdk::InModule for RuntimeProfileInviteCodeAdminProcedureResult { type Module = super::RemoteModule; } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_upsert_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_upsert_input_type.rs index 77daf412..9b394d43 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_upsert_input_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_upsert_input_type.rs @@ -2,7 +2,13 @@ // 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 spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] @@ -13,6 +19,7 @@ pub struct RuntimeProfileInviteCodeAdminUpsertInput { pub updated_at_micros: i64, } + impl __sdk::InModule for RuntimeProfileInviteCodeAdminUpsertInput { type Module = super::RemoteModule; } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_snapshot_type.rs index 36ea09ee..66d378f0 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_snapshot_type.rs @@ -2,7 +2,13 @@ // 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 spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] @@ -14,6 +20,7 @@ pub struct RuntimeProfileInviteCodeSnapshot { pub updated_at_micros: i64, } + impl __sdk::InModule for RuntimeProfileInviteCodeSnapshot { type Module = super::RemoteModule; } diff --git a/server-rs/crates/spacetime-module/src/puzzle.rs b/server-rs/crates/spacetime-module/src/puzzle.rs index a8ed020f..caa00342 100644 --- a/server-rs/crates/spacetime-module/src/puzzle.rs +++ b/server-rs/crates/spacetime-module/src/puzzle.rs @@ -1911,7 +1911,7 @@ fn claim_puzzle_work_point_incentive_tx( return Err("无权领取该作品的积分激励".to_string()); } - let claimable_points = puzzle_point_incentive_claimable_points( + let claimable_points = module_puzzle::puzzle_point_incentive_claimable_points( row.point_incentive_total_half_points, row.point_incentive_claimed_points, ); @@ -2571,9 +2571,9 @@ fn upsert_puzzle_profile_save_archive( "runtimeKind": "puzzle", "runId": run.run_id, "entryProfileId": run.entry_profile_id, - "currentProfileId": target.profile_id, + "currentProfileId": target.profile_id.clone(), "currentLevelIndex": target.level_index, - "currentLevelId": target.level_id, + "currentLevelId": target.level_id.clone(), "status": target.status.as_str(), })) .unwrap_or_else(|_| "{}".to_string()); @@ -2613,6 +2613,8 @@ fn resolve_puzzle_archive_target( 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 { @@ -2697,12 +2699,6 @@ 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, @@ -2749,9 +2745,10 @@ fn accrue_puzzle_point_incentive( 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_total_half_points: module_puzzle::puzzle_point_incentive_total_after_spend( + row.point_incentive_total_half_points, + spent_points, + ), point_incentive_claimed_points: row.point_incentive_claimed_points, anchor_pack_json: row.anchor_pack_json.clone(), publish_ready: row.publish_ready, diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 49ce361c..9534bfa2 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -2071,12 +2071,13 @@ export function PlatformEntryFlowShellImpl({ ? mergePuzzleServiceRuntimeState(currentRun, run) : currentRun, ); + void platformBootstrap.refreshSaveArchives(); } catch (error) { setPuzzleError( resolvePuzzleErrorMessage(error, '同步拼图失败状态失败。'), ); } - }, [puzzleRun, resolvePuzzleErrorMessage, setPuzzleError]); + }, [platformBootstrap, puzzleRun, resolvePuzzleErrorMessage, setPuzzleError]); const usePuzzleProp = useCallback( async (propKind: PuzzleRuntimePropKind) => { @@ -2677,6 +2678,7 @@ export function PlatformEntryFlowShellImpl({ syncUpdatedPublicWorkDetail( mapPuzzleWorkToPublicWorkDetail(updatedWork), ); + void platformBootstrap.refreshProfileDashboard(); }) .catch((error) => { setPuzzleError( @@ -2690,6 +2692,7 @@ export function PlatformEntryFlowShellImpl({ }, [ claimingPuzzlePointIncentiveProfileId, + platformBootstrap, resolvePuzzleErrorMessage, runProtectedAction, setPuzzleError,