@@ -36,11 +36,12 @@ use shared_contracts::{
|
||||
},
|
||||
puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse},
|
||||
puzzle_runtime::{
|
||||
DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse, PuzzleCellPositionResponse,
|
||||
PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse, PuzzlePieceStateResponse,
|
||||
PuzzleRecommendedNextWorkResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse,
|
||||
PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest,
|
||||
SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest, UsePuzzleRuntimePropRequest,
|
||||
AdvancePuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse,
|
||||
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse,
|
||||
PuzzlePieceStateResponse, PuzzleRecommendedNextWorkResponse, PuzzleRunResponse,
|
||||
PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest,
|
||||
SubmitPuzzleLeaderboardRequest, SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest,
|
||||
UsePuzzleRuntimePropRequest,
|
||||
},
|
||||
puzzle_works::{
|
||||
PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
|
||||
@@ -1367,14 +1368,34 @@ pub async fn advance_puzzle_next_level(
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
payload: Result<Json<AdvancePuzzleNextLevelRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
let payload = match payload {
|
||||
Ok(Json(payload)) => payload,
|
||||
Err(error) if error.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE => {
|
||||
AdvancePuzzleNextLevelRequest {
|
||||
target_profile_id: None,
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUZZLE_RUNTIME_PROVIDER,
|
||||
"message": error.body_text(),
|
||||
})),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.advance_puzzle_next_level(spacetime_client::PuzzleRunNextLevelRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
target_profile_id: payload.target_profile_id,
|
||||
advanced_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -227,7 +227,7 @@ pub fn build_system_username(prefix: &str, sequence: u64) -> String {
|
||||
format!("{prefix}_{sequence:08}")
|
||||
}
|
||||
|
||||
// 公开叙世号是稳定的公开检索键,不替代内部 user_id,仅用于展示、分享与搜索。
|
||||
// 公开百梦号是稳定的公开检索键,不替代内部 user_id,仅用于展示、分享与搜索。
|
||||
pub fn build_public_user_code(sequence: u64) -> String {
|
||||
format!("SY-{sequence:08}")
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ impl fmt::Display for PasswordEntryError {
|
||||
Self::InvalidDisplayName => f.write_str("昵称格式不正确"),
|
||||
Self::InvalidAvatarDataUrl => f.write_str("头像图片格式不正确"),
|
||||
Self::EmptyProfileUpdate => f.write_str("请至少修改昵称或头像"),
|
||||
Self::InvalidPublicUserCode => f.write_str("叙世号格式不正确"),
|
||||
Self::InvalidPublicUserCode => f.write_str("百梦号格式不正确"),
|
||||
Self::InvalidCredentials => f.write_str("手机号或密码错误"),
|
||||
Self::UserNotFound => f.write_str("用户不存在"),
|
||||
Self::Store(message) | Self::PasswordHash(message) => f.write_str(message),
|
||||
|
||||
@@ -1373,8 +1373,8 @@ pub fn advance_to_new_work_first_level_at(
|
||||
return Err(PuzzleFieldError::InvalidOperation);
|
||||
}
|
||||
|
||||
// 中文注释:跨作品代表进入一个新作品,关卡序号、切割规格和倒计时都从第 1 关重新开始。
|
||||
let next_level_index = 1;
|
||||
// 中文注释:跨作品只切换到候选作品的第一张图,运行时关卡序号和难度循环继续累进。
|
||||
let next_level_index = run.current_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(
|
||||
@@ -1391,8 +1391,8 @@ pub fn advance_to_new_work_first_level_at(
|
||||
|
||||
Ok(PuzzleRunSnapshot {
|
||||
run_id: run.run_id.clone(),
|
||||
entry_profile_id: next_profile.profile_id.clone(),
|
||||
cleared_level_count: 0,
|
||||
entry_profile_id: run.entry_profile_id.clone(),
|
||||
cleared_level_count: run.cleared_level_count,
|
||||
current_level_index: next_level_index,
|
||||
current_grid_size: grid_size,
|
||||
played_profile_ids,
|
||||
@@ -2998,7 +2998,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advance_to_new_work_first_level_restarts_level_progress() {
|
||||
fn advance_to_new_work_first_profile_level_keeps_runtime_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");
|
||||
@@ -3011,14 +3011,14 @@ mod tests {
|
||||
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);
|
||||
assert_eq!(next_run.entry_profile_id, "entry");
|
||||
assert_eq!(next_run.cleared_level_count, 3);
|
||||
assert_eq!(next_run.current_level_index, 4);
|
||||
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);
|
||||
assert_eq!(next_level.level_index, 4);
|
||||
assert_eq!(next_level.grid_size, 5);
|
||||
assert_eq!(next_level.time_limit_ms, 210_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -213,6 +213,8 @@ pub struct PuzzleRunDragInput {
|
||||
pub struct PuzzleRunNextLevelInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
#[serde(default)]
|
||||
pub target_profile_id: Option<String>,
|
||||
pub advanced_at_micros: i64,
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ impl std::fmt::Display for RuntimeProfileFieldError {
|
||||
Self::InvalidWalletAmount => f.write_str("profile.wallet_amount 必须大于 0"),
|
||||
Self::WalletAmountOverflow => f.write_str("profile.wallet_amount 超出上限"),
|
||||
Self::WalletBalanceOverflow => f.write_str("profile.wallet_balance 超出上限"),
|
||||
Self::InsufficientWalletBalance => f.write_str("叙世币余额不足"),
|
||||
Self::InsufficientWalletBalance => f.write_str("光点余额不足"),
|
||||
Self::MissingInviteCode => f.write_str("referral.invite_code 不能为空"),
|
||||
Self::MissingRedeemCode => f.write_str("兑换码不能为空"),
|
||||
Self::RedeemCodeDisabled => f.write_str("兑换码已停用"),
|
||||
|
||||
@@ -22,57 +22,57 @@ pub fn runtime_profile_recharge_point_products() -> Vec<RuntimeProfileRechargePr
|
||||
vec![
|
||||
build_points_recharge_product(
|
||||
"points_60",
|
||||
"60叙世币",
|
||||
"60光点",
|
||||
600,
|
||||
60,
|
||||
60,
|
||||
"首充双倍",
|
||||
"首充送60叙世币",
|
||||
"首充送60光点",
|
||||
),
|
||||
build_points_recharge_product(
|
||||
"points_180",
|
||||
"180叙世币",
|
||||
"180光点",
|
||||
1800,
|
||||
180,
|
||||
180,
|
||||
"首充双倍",
|
||||
"首充送180叙世币",
|
||||
"首充送180光点",
|
||||
),
|
||||
build_points_recharge_product(
|
||||
"points_300",
|
||||
"300叙世币",
|
||||
"300光点",
|
||||
3000,
|
||||
300,
|
||||
300,
|
||||
"首充双倍",
|
||||
"首充送300叙世币",
|
||||
"首充送300光点",
|
||||
),
|
||||
build_points_recharge_product(
|
||||
"points_680",
|
||||
"680叙世币",
|
||||
"680光点",
|
||||
6800,
|
||||
680,
|
||||
680,
|
||||
"首充双倍",
|
||||
"首充送680叙世币",
|
||||
"首充送680光点",
|
||||
),
|
||||
build_points_recharge_product(
|
||||
"points_1280",
|
||||
"1280叙世币",
|
||||
"1280光点",
|
||||
12800,
|
||||
1280,
|
||||
1280,
|
||||
"首充双倍",
|
||||
"首充送1280叙世币",
|
||||
"首充送1280光点",
|
||||
),
|
||||
build_points_recharge_product(
|
||||
"points_3280",
|
||||
"3280叙世币",
|
||||
"3280光点",
|
||||
32800,
|
||||
3280,
|
||||
3280,
|
||||
"首充双倍",
|
||||
"首充送3280叙世币",
|
||||
"首充送3280光点",
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -121,7 +121,7 @@ pub fn runtime_profile_membership_benefits() -> Vec<RuntimeProfileMembershipBene
|
||||
year_value: "¥248".to_string(),
|
||||
},
|
||||
RuntimeProfileMembershipBenefitSnapshot {
|
||||
benefit_name: "免叙世币回合数".to_string(),
|
||||
benefit_name: "免光点回合数".to_string(),
|
||||
normal_value: "30".to_string(),
|
||||
month_value: "100".to_string(),
|
||||
season_value: "100".to_string(),
|
||||
@@ -457,14 +457,14 @@ mod tests {
|
||||
|
||||
assert_eq!(point_products.len(), 6);
|
||||
assert_eq!(point_products[0].product_id, "points_60");
|
||||
assert_eq!(point_products[0].title, "60叙世币");
|
||||
assert_eq!(point_products[0].title, "60光点");
|
||||
assert_eq!(point_products[0].price_cents, 600);
|
||||
assert_eq!(point_products[0].bonus_points, 60);
|
||||
assert_eq!(point_products[0].description, "首充送60叙世币");
|
||||
assert_eq!(point_products[0].description, "首充送60光点");
|
||||
assert_eq!(point_products[5].product_id, "points_3280");
|
||||
assert_eq!(point_products[5].price_cents, 32800);
|
||||
assert_eq!(point_products[5].bonus_points, 3280);
|
||||
assert_eq!(point_products[5].description, "首充送3280叙世币");
|
||||
assert_eq!(point_products[5].description, "首充送3280光点");
|
||||
assert_eq!(membership_products.len(), 3);
|
||||
assert_eq!(membership_products[0].title, "月卡");
|
||||
assert_eq!(membership_products[0].price_cents, 2800);
|
||||
@@ -474,7 +474,7 @@ mod tests {
|
||||
assert!(
|
||||
benefits
|
||||
.iter()
|
||||
.any(|benefit| benefit.benefit_name == "免叙世币回合数")
|
||||
.any(|benefit| benefit.benefit_name == "免光点回合数")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,13 @@ pub struct DragPuzzlePieceRequest {
|
||||
pub target_col: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdvancePuzzleNextLevelRequest {
|
||||
#[serde(default)]
|
||||
pub target_profile_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UsePuzzleRuntimePropRequest {
|
||||
|
||||
@@ -4783,6 +4783,7 @@ pub struct PuzzleRunDragRecordInput {
|
||||
pub struct PuzzleRunNextLevelRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub target_profile_id: Option<String>,
|
||||
pub advanced_at_micros: i64,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
// This was generated using spacetimedb cli version 2.1.0 (commit 10a4779b1338eff3708493d87496b51842a7c412).
|
||||
// This was generated using spacetimedb cli version 2.1.0 (commit 6981f48b4bc1a71c8dd9bdfe5a2c343f6370243d).
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
@@ -9,6 +9,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
pub struct PuzzleRunNextLevelInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub target_profile_id: Option<String>,
|
||||
pub advanced_at_micros: i64,
|
||||
}
|
||||
|
||||
|
||||
@@ -559,6 +559,7 @@ impl SpacetimeClient {
|
||||
let procedure_input = PuzzleRunNextLevelInput {
|
||||
run_id: input.run_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
target_profile_id: input.target_profile_id,
|
||||
advanced_at_micros: input.advanced_at_micros,
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct CustomWorldProfile {
|
||||
owner_user_id: String,
|
||||
// 作品公开编号是稳定分享键,第一次发布时分配,后续重复发布沿用。
|
||||
public_work_code: Option<String>,
|
||||
// 作者公开叙世号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。
|
||||
// 作者公开百梦号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。
|
||||
author_public_user_code: Option<String>,
|
||||
source_agent_session_id: Option<String>,
|
||||
publication_status: CustomWorldPublicationStatus,
|
||||
|
||||
@@ -1769,17 +1769,36 @@ fn advance_puzzle_next_level_tx(
|
||||
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 candidates = if same_work_next_profile.is_none() {
|
||||
list_published_puzzle_profiles(ctx)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let similar_work_next_profile = if same_work_next_profile.is_none() {
|
||||
let candidates = list_published_puzzle_profiles(ctx)?;
|
||||
select_next_profiles(
|
||||
let selected_candidates = select_next_profiles(
|
||||
¤t_profile,
|
||||
¤t_run.played_profile_ids,
|
||||
&candidates,
|
||||
1,
|
||||
3,
|
||||
);
|
||||
Some(
|
||||
if let Some(target_profile_id) = input.target_profile_id.as_ref().and_then(|value| {
|
||||
let trimmed = value.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
}) {
|
||||
selected_candidates
|
||||
.into_iter()
|
||||
.find(|candidate| candidate.profile_id == target_profile_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| "目标拼图作品不在当前下一关候选中".to_string())?
|
||||
} else {
|
||||
selected_candidates
|
||||
.into_iter()
|
||||
.next()
|
||||
.cloned()
|
||||
.ok_or_else(|| "没有可用的下一关候选".to_string())?
|
||||
},
|
||||
)
|
||||
.into_iter()
|
||||
.next()
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user