|
|
|
|
@@ -1,28 +1,31 @@
|
|
|
|
|
use crate::runtime::{
|
|
|
|
|
ProfilePlayedWorkUpsertInput, PublicWorkPlayRecordInput, add_profile_observed_play_time,
|
|
|
|
|
count_recent_public_work_plays, record_public_work_like, record_public_work_play,
|
|
|
|
|
upsert_profile_played_work, PublicWorkLikeRecordInput,
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
use module_puzzle::{
|
|
|
|
|
PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageFinalizeInput, PuzzleAgentMessageKind,
|
|
|
|
|
PUZZLE_MAX_TAG_COUNT, PUZZLE_NEXT_LEVEL_MODE_NONE, PUZZLE_NEXT_LEVEL_MODE_SAME_WORK,
|
|
|
|
|
PUZZLE_NEXT_LEVEL_MODE_SIMILAR_WORKS, PuzzleAgentMessageFinalizeInput, PuzzleAgentMessageKind,
|
|
|
|
|
PuzzleAgentMessageRole, PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput,
|
|
|
|
|
PuzzleAgentSessionGetInput, PuzzleAgentSessionProcedureResult, PuzzleAgentSessionSnapshot,
|
|
|
|
|
PuzzleAgentStage, PuzzleAnchorPack, PuzzleDraftCompileInput, PuzzleFormDraftSaveInput,
|
|
|
|
|
PuzzleGeneratedImageCandidate, PuzzleGeneratedImagesSaveInput, PuzzleLeaderboardEntry,
|
|
|
|
|
PuzzleLeaderboardSubmitInput,
|
|
|
|
|
PuzzlePublicationStatus, PuzzlePublishInput, PuzzleResultDraft, PuzzleRunDragInput,
|
|
|
|
|
PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunPauseInput, PuzzleRunProcedureResult,
|
|
|
|
|
PuzzleRunPropInput, PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput,
|
|
|
|
|
PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput, PuzzleWorkDeleteInput,
|
|
|
|
|
PuzzleWorkGetInput, PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput,
|
|
|
|
|
PuzzleWorkProcedureResult, PuzzleWorkProfile, PuzzleWorkRemixInput, PuzzleWorkUpsertInput,
|
|
|
|
|
PuzzleWorksListInput, PuzzleWorksProcedureResult,
|
|
|
|
|
PuzzleLeaderboardSubmitInput, PuzzlePublicationStatus, PuzzlePublishInput,
|
|
|
|
|
PuzzleRecommendedNextWork, PuzzleResultDraft, PuzzleRunDragInput, PuzzleRunGetInput,
|
|
|
|
|
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_profile, selected_puzzle_level,
|
|
|
|
|
replace_puzzle_level, resolve_puzzle_grid_size, select_next_profiles,
|
|
|
|
|
selected_profile_level_after_runtime_level, selected_puzzle_level, tag_similarity_score,
|
|
|
|
|
};
|
|
|
|
|
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};
|
|
|
|
|
|
|
|
|
|
@@ -889,6 +892,11 @@ fn save_puzzle_generated_images_tx(
|
|
|
|
|
) -> Result<PuzzleAgentSessionSnapshot, String> {
|
|
|
|
|
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
|
|
|
|
|
let mut draft = deserialize_draft_required(&row.draft_json)?;
|
|
|
|
|
if let Some(levels) = deserialize_optional_levels_input(input.levels_json.as_deref())? {
|
|
|
|
|
// 中文注释:结果页新增关卡可能还没等到自动保存,生成图时以本次 action 携带的关卡快照作为写回目标。
|
|
|
|
|
draft.levels = levels;
|
|
|
|
|
module_puzzle::sync_primary_level_fields(&mut draft);
|
|
|
|
|
}
|
|
|
|
|
let candidates: Vec<PuzzleGeneratedImageCandidate> = json_from_str(&input.candidates_json)
|
|
|
|
|
.map_err(|error| format!("拼图候选图 JSON 非法: {error}"))?;
|
|
|
|
|
if candidates.is_empty() {
|
|
|
|
|
@@ -1539,12 +1547,7 @@ fn start_puzzle_run_tx(
|
|
|
|
|
current_profile_id.as_str(),
|
|
|
|
|
current_grid_size,
|
|
|
|
|
);
|
|
|
|
|
run.recommended_next_profile_id = select_next_profile(
|
|
|
|
|
&entry_profile,
|
|
|
|
|
&run.played_profile_ids,
|
|
|
|
|
&list_published_puzzle_profiles(ctx)?,
|
|
|
|
|
)
|
|
|
|
|
.map(|value| value.profile_id.clone());
|
|
|
|
|
refresh_next_level_handoff(ctx, &mut run)?;
|
|
|
|
|
|
|
|
|
|
record_public_work_play(
|
|
|
|
|
ctx,
|
|
|
|
|
@@ -1576,6 +1579,7 @@ fn get_puzzle_run_tx(
|
|
|
|
|
deserialize_run(&row.snapshot_json)?,
|
|
|
|
|
micros_to_millis(now_micros),
|
|
|
|
|
);
|
|
|
|
|
refresh_next_level_handoff(ctx, &mut run)?;
|
|
|
|
|
if serialize_json(&run) != row.snapshot_json {
|
|
|
|
|
replace_puzzle_runtime_run(ctx, &row, &run, now_micros);
|
|
|
|
|
}
|
|
|
|
|
@@ -1608,7 +1612,7 @@ fn swap_puzzle_pieces_tx(
|
|
|
|
|
micros_to_millis(input.swapped_at_micros),
|
|
|
|
|
)
|
|
|
|
|
.map_err(|error| error.to_string())?;
|
|
|
|
|
refresh_next_profile_recommendation(ctx, &mut next_run)?;
|
|
|
|
|
refresh_next_level_handoff(ctx, &mut next_run)?;
|
|
|
|
|
if let Some((profile_id, grid_size)) = next_run
|
|
|
|
|
.current_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
@@ -1640,7 +1644,7 @@ fn drag_puzzle_piece_or_group_tx(
|
|
|
|
|
micros_to_millis(input.dragged_at_micros),
|
|
|
|
|
)
|
|
|
|
|
.map_err(|error| error.to_string())?;
|
|
|
|
|
refresh_next_profile_recommendation(ctx, &mut next_run)?;
|
|
|
|
|
refresh_next_level_handoff(ctx, &mut next_run)?;
|
|
|
|
|
if let Some((profile_id, grid_size)) = next_run
|
|
|
|
|
.current_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
@@ -1671,21 +1675,28 @@ fn advance_puzzle_next_level_tx(
|
|
|
|
|
if current_level.status != PuzzleRuntimeLevelStatus::Cleared {
|
|
|
|
|
return Err("当前关卡尚未通关".to_string());
|
|
|
|
|
}
|
|
|
|
|
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 candidates = list_published_puzzle_profiles(ctx)?;
|
|
|
|
|
let next_profile = select_next_profile(
|
|
|
|
|
¤t_profile,
|
|
|
|
|
¤t_run.played_profile_ids,
|
|
|
|
|
&candidates,
|
|
|
|
|
)
|
|
|
|
|
.ok_or_else(|| "没有可用的下一关候选".to_string())?
|
|
|
|
|
.clone();
|
|
|
|
|
let current_profile_row = ctx
|
|
|
|
|
.db
|
|
|
|
|
.puzzle_work_profile()
|
|
|
|
|
.profile_id()
|
|
|
|
|
.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()
|
|
|
|
|
})
|
|
|
|
|
.ok_or_else(|| "没有可用的下一关候选".to_string())?;
|
|
|
|
|
let mut next_run = module_puzzle::advance_next_level_at(
|
|
|
|
|
¤t_run,
|
|
|
|
|
&next_profile,
|
|
|
|
|
@@ -1701,9 +1712,7 @@ fn advance_puzzle_next_level_tx(
|
|
|
|
|
&next_profile_id,
|
|
|
|
|
next_grid_size,
|
|
|
|
|
);
|
|
|
|
|
next_run.recommended_next_profile_id =
|
|
|
|
|
select_next_profile(&next_profile, &next_run.played_profile_ids, &candidates)
|
|
|
|
|
.map(|value| value.profile_id.clone());
|
|
|
|
|
refresh_next_level_handoff(ctx, &mut next_run)?;
|
|
|
|
|
|
|
|
|
|
if let Some(next_profile_row) = ctx
|
|
|
|
|
.db
|
|
|
|
|
@@ -1744,8 +1753,9 @@ fn update_puzzle_run_pause_tx(
|
|
|
|
|
micros_to_millis(input.updated_at_micros),
|
|
|
|
|
)
|
|
|
|
|
.map_err(|error| error.to_string())?;
|
|
|
|
|
replace_puzzle_runtime_run(ctx, &row, &next_run, input.updated_at_micros);
|
|
|
|
|
let mut hydrated_run = next_run;
|
|
|
|
|
refresh_next_level_handoff(ctx, &mut hydrated_run)?;
|
|
|
|
|
replace_puzzle_runtime_run(ctx, &row, &hydrated_run, input.updated_at_micros);
|
|
|
|
|
if let Some((profile_id, grid_size)) = hydrated_run
|
|
|
|
|
.current_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
@@ -1774,6 +1784,11 @@ fn use_puzzle_runtime_prop_tx(
|
|
|
|
|
micros_to_millis(input.used_at_micros),
|
|
|
|
|
)
|
|
|
|
|
.map_err(|error| error.to_string())?,
|
|
|
|
|
"extendTime" | "extend_time" => module_puzzle::extend_failed_puzzle_time_at(
|
|
|
|
|
¤t_run,
|
|
|
|
|
micros_to_millis(input.used_at_micros),
|
|
|
|
|
)
|
|
|
|
|
.map_err(|error| error.to_string())?,
|
|
|
|
|
"hint" => module_puzzle::set_puzzle_run_paused_at(
|
|
|
|
|
¤t_run,
|
|
|
|
|
false,
|
|
|
|
|
@@ -1788,8 +1803,9 @@ fn use_puzzle_runtime_prop_tx(
|
|
|
|
|
.map_err(|error| error.to_string())?,
|
|
|
|
|
_ => return Err("未知拼图道具".to_string()),
|
|
|
|
|
};
|
|
|
|
|
replace_puzzle_runtime_run(ctx, &row, &next_run, input.used_at_micros);
|
|
|
|
|
let mut hydrated_run = next_run;
|
|
|
|
|
refresh_next_level_handoff(ctx, &mut hydrated_run)?;
|
|
|
|
|
replace_puzzle_runtime_run(ctx, &row, &hydrated_run, input.used_at_micros);
|
|
|
|
|
if let Some((profile_id, grid_size)) = hydrated_run
|
|
|
|
|
.current_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
@@ -1883,6 +1899,7 @@ fn submit_puzzle_leaderboard_entry_tx(
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
run.leaderboard_entries = leaderboard_entries;
|
|
|
|
|
refresh_next_level_handoff(ctx, &mut run)?;
|
|
|
|
|
replace_puzzle_runtime_run(ctx, &row, &run, input.submitted_at_micros);
|
|
|
|
|
Ok(run)
|
|
|
|
|
}
|
|
|
|
|
@@ -1891,6 +1908,14 @@ fn is_frontend_puzzle_level_candidate(run: &PuzzleRunSnapshot, profile_id: &str)
|
|
|
|
|
run.recommended_next_profile_id
|
|
|
|
|
.as_ref()
|
|
|
|
|
.is_some_and(|candidate_profile_id| candidate_profile_id == profile_id)
|
|
|
|
|
|| run
|
|
|
|
|
.next_level_profile_id
|
|
|
|
|
.as_ref()
|
|
|
|
|
.is_some_and(|candidate_profile_id| candidate_profile_id == profile_id)
|
|
|
|
|
|| run
|
|
|
|
|
.recommended_next_works
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|candidate| candidate.profile_id == profile_id)
|
|
|
|
|
|| run
|
|
|
|
|
.played_profile_ids
|
|
|
|
|
.iter()
|
|
|
|
|
@@ -2328,6 +2353,7 @@ fn insert_puzzle_runtime_run(
|
|
|
|
|
created_at: timestamp,
|
|
|
|
|
updated_at: timestamp,
|
|
|
|
|
});
|
|
|
|
|
upsert_puzzle_profile_save_archive(ctx, run, owner_user_id, created_at_micros)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -2356,6 +2382,75 @@ fn replace_puzzle_runtime_run(
|
|
|
|
|
created_at: current.created_at,
|
|
|
|
|
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
|
|
|
|
});
|
|
|
|
|
if let Err(error) =
|
|
|
|
|
upsert_puzzle_profile_save_archive(ctx, run, ¤t.owner_user_id, updated_at_micros)
|
|
|
|
|
{
|
|
|
|
|
log::warn!("拼图存档投影同步失败: {}", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn upsert_puzzle_profile_save_archive(
|
|
|
|
|
ctx: &TxContext,
|
|
|
|
|
run: &PuzzleRunSnapshot,
|
|
|
|
|
user_id: &str,
|
|
|
|
|
saved_at_micros: i64,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
let user_id = user_id.trim();
|
|
|
|
|
if user_id.is_empty() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
let Some(current_level) = run.current_level.as_ref() else {
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
let world_key = format!("puzzle:{}", run.entry_profile_id);
|
|
|
|
|
|
|
|
|
|
// 中文注释:拼图存档只保存恢复入口所需的最小运行态索引,棋盘真相继续放在 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(),
|
|
|
|
|
}))
|
|
|
|
|
.unwrap_or_else(|_| "{}".to_string());
|
|
|
|
|
|
|
|
|
|
upsert_profile_save_archive(
|
|
|
|
|
ctx,
|
|
|
|
|
ProfileSaveArchiveUpsertInput {
|
|
|
|
|
user_id: user_id.to_string(),
|
|
|
|
|
world_key,
|
|
|
|
|
owner_user_id: resolve_puzzle_current_owner_user_id(ctx, ¤t_level.profile_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(),
|
|
|
|
|
bottom_tab: "puzzle".to_string(),
|
|
|
|
|
game_state_json,
|
|
|
|
|
current_story_json: None,
|
|
|
|
|
saved_at_micros,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve_puzzle_current_owner_user_id(ctx: &TxContext, profile_id: &str) -> Option<String> {
|
|
|
|
|
ctx.db
|
|
|
|
|
.puzzle_work_profile()
|
|
|
|
|
.profile_id()
|
|
|
|
|
.find(&profile_id.to_string())
|
|
|
|
|
.map(|row| row.owner_user_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn puzzle_archive_summary_text(status: PuzzleRuntimeLevelStatus) -> String {
|
|
|
|
|
match status {
|
|
|
|
|
PuzzleRuntimeLevelStatus::Cleared => "关卡已完成",
|
|
|
|
|
PuzzleRuntimeLevelStatus::Failed => "关卡失败",
|
|
|
|
|
PuzzleRuntimeLevelStatus::Playing => "拼图进行中",
|
|
|
|
|
}
|
|
|
|
|
.to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn increment_puzzle_profile_play_count(
|
|
|
|
|
@@ -2439,14 +2534,33 @@ fn list_published_puzzle_profiles(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfi
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn refresh_next_profile_recommendation(
|
|
|
|
|
ctx: &TxContext,
|
|
|
|
|
run: &mut PuzzleRunSnapshot,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
fn reset_next_level_handoff(run: &mut PuzzleRunSnapshot) {
|
|
|
|
|
run.recommended_next_profile_id = None;
|
|
|
|
|
run.next_level_mode = PUZZLE_NEXT_LEVEL_MODE_NONE.to_string();
|
|
|
|
|
run.next_level_profile_id = None;
|
|
|
|
|
run.next_level_id = None;
|
|
|
|
|
run.recommended_next_works = Vec::new();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_recommended_next_work(
|
|
|
|
|
current_profile: &PuzzleWorkProfile,
|
|
|
|
|
candidate: &PuzzleWorkProfile,
|
|
|
|
|
) -> PuzzleRecommendedNextWork {
|
|
|
|
|
PuzzleRecommendedNextWork {
|
|
|
|
|
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: tag_similarity_score(¤t_profile.theme_tags, &candidate.theme_tags),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn refresh_next_level_handoff(ctx: &TxContext, run: &mut PuzzleRunSnapshot) -> Result<(), String> {
|
|
|
|
|
let current_level = match run.current_level.as_ref() {
|
|
|
|
|
Some(value) => value,
|
|
|
|
|
None => {
|
|
|
|
|
run.recommended_next_profile_id = None;
|
|
|
|
|
reset_next_level_handoff(run);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
@@ -2457,12 +2571,41 @@ fn refresh_next_profile_recommendation(
|
|
|
|
|
.find(¤t_level.profile_id)
|
|
|
|
|
.ok_or_else(|| "当前拼图作品不存在".to_string())?,
|
|
|
|
|
)?;
|
|
|
|
|
run.recommended_next_profile_id = select_next_profile(
|
|
|
|
|
¤t_profile,
|
|
|
|
|
&run.played_profile_ids,
|
|
|
|
|
&list_published_puzzle_profiles(ctx)?,
|
|
|
|
|
)
|
|
|
|
|
.map(|value| value.profile_id.clone());
|
|
|
|
|
if current_level.status != PuzzleRuntimeLevelStatus::Cleared {
|
|
|
|
|
reset_next_level_handoff(run);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(next_level) =
|
|
|
|
|
selected_profile_level_after_runtime_level(¤t_profile, current_level)
|
|
|
|
|
{
|
|
|
|
|
run.next_level_mode = PUZZLE_NEXT_LEVEL_MODE_SAME_WORK.to_string();
|
|
|
|
|
run.next_level_profile_id = Some(current_profile.profile_id.clone());
|
|
|
|
|
run.next_level_id = Some(next_level.level_id);
|
|
|
|
|
run.recommended_next_profile_id = Some(current_profile.profile_id.clone());
|
|
|
|
|
run.recommended_next_works = Vec::new();
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let candidates = list_published_puzzle_profiles(ctx)?;
|
|
|
|
|
let recommended_next_works =
|
|
|
|
|
select_next_profiles(¤t_profile, &run.played_profile_ids, &candidates, 3)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|candidate| build_recommended_next_work(¤t_profile, candidate))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
if recommended_next_works.is_empty() {
|
|
|
|
|
reset_next_level_handoff(run);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
run.next_level_mode = PUZZLE_NEXT_LEVEL_MODE_SIMILAR_WORKS.to_string();
|
|
|
|
|
run.next_level_profile_id = recommended_next_works
|
|
|
|
|
.first()
|
|
|
|
|
.map(|candidate| candidate.profile_id.clone());
|
|
|
|
|
run.next_level_id = None;
|
|
|
|
|
run.recommended_next_profile_id = run.next_level_profile_id.clone();
|
|
|
|
|
run.recommended_next_works = recommended_next_works;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -2640,6 +2783,10 @@ mod tests {
|
|
|
|
|
previous_level_tags: vec!["蒸汽城市".to_string()],
|
|
|
|
|
current_level: None,
|
|
|
|
|
recommended_next_profile_id: None,
|
|
|
|
|
next_level_mode: 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 serialized = serialize_json(&snapshot);
|
|
|
|
|
|