|
|
|
|
@@ -186,6 +186,9 @@ pub fn compile_result_draft_from_seed(
|
|
|
|
|
level_name: level_name.clone(),
|
|
|
|
|
picture_description,
|
|
|
|
|
picture_reference: None,
|
|
|
|
|
ui_background_prompt: None,
|
|
|
|
|
ui_background_image_src: None,
|
|
|
|
|
ui_background_image_object_key: None,
|
|
|
|
|
background_music: None,
|
|
|
|
|
candidates: Vec::new(),
|
|
|
|
|
selected_candidate_id: None,
|
|
|
|
|
@@ -243,6 +246,9 @@ pub fn build_form_draft_from_parts(
|
|
|
|
|
level_name: String::new(),
|
|
|
|
|
picture_description: picture_description.clone().unwrap_or_default(),
|
|
|
|
|
picture_reference: None,
|
|
|
|
|
ui_background_prompt: None,
|
|
|
|
|
ui_background_image_src: None,
|
|
|
|
|
ui_background_image_object_key: None,
|
|
|
|
|
background_music: None,
|
|
|
|
|
candidates: Vec::new(),
|
|
|
|
|
selected_candidate_id: None,
|
|
|
|
|
@@ -349,6 +355,9 @@ pub fn normalize_puzzle_draft(mut draft: PuzzleResultDraft) -> PuzzleResultDraft
|
|
|
|
|
&draft.summary,
|
|
|
|
|
),
|
|
|
|
|
picture_reference: None,
|
|
|
|
|
ui_background_prompt: None,
|
|
|
|
|
ui_background_image_src: None,
|
|
|
|
|
ui_background_image_object_key: None,
|
|
|
|
|
background_music: None,
|
|
|
|
|
candidates: draft.candidates.clone(),
|
|
|
|
|
selected_candidate_id: draft.selected_candidate_id.clone(),
|
|
|
|
|
@@ -436,6 +445,9 @@ pub fn append_blank_puzzle_level(draft: &PuzzleResultDraft) -> PuzzleResultDraft
|
|
|
|
|
),
|
|
|
|
|
picture_description,
|
|
|
|
|
picture_reference: None,
|
|
|
|
|
ui_background_prompt: None,
|
|
|
|
|
ui_background_image_src: None,
|
|
|
|
|
ui_background_image_object_key: None,
|
|
|
|
|
background_music: None,
|
|
|
|
|
candidates: Vec::new(),
|
|
|
|
|
selected_candidate_id: None,
|
|
|
|
|
@@ -767,6 +779,13 @@ pub fn resolve_puzzle_level_time_limit_ms(grid_size: u32) -> u64 {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn first_profile_level(profile: &PuzzleWorkProfile) -> Option<PuzzleDraftLevel> {
|
|
|
|
|
normalize_puzzle_levels(profile.levels.clone(), &profile.theme_tags)
|
|
|
|
|
.unwrap_or_else(|_| profile.levels.clone())
|
|
|
|
|
.into_iter()
|
|
|
|
|
.next()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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_by_index(level.level_index)
|
|
|
|
|
@@ -1027,6 +1046,7 @@ pub fn start_run_with_shuffle_seed_at(
|
|
|
|
|
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)?;
|
|
|
|
|
let current_profile_level = first_profile_level(entry_profile);
|
|
|
|
|
Ok(PuzzleRunSnapshot {
|
|
|
|
|
run_id: run_id.clone(),
|
|
|
|
|
entry_profile_id: entry_profile.profile_id.clone(),
|
|
|
|
|
@@ -1038,9 +1058,8 @@ pub fn start_run_with_shuffle_seed_at(
|
|
|
|
|
current_level: Some(PuzzleRuntimeLevelSnapshot {
|
|
|
|
|
run_id,
|
|
|
|
|
level_index,
|
|
|
|
|
level_id: entry_profile
|
|
|
|
|
.levels
|
|
|
|
|
.first()
|
|
|
|
|
level_id: current_profile_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|level| level.level_id.clone()),
|
|
|
|
|
grid_size,
|
|
|
|
|
profile_id: entry_profile.profile_id.clone(),
|
|
|
|
|
@@ -1048,6 +1067,12 @@ pub fn start_run_with_shuffle_seed_at(
|
|
|
|
|
author_display_name: entry_profile.author_display_name.clone(),
|
|
|
|
|
theme_tags: entry_profile.theme_tags.clone(),
|
|
|
|
|
cover_image_src: entry_profile.cover_image_src.clone(),
|
|
|
|
|
ui_background_image_src: current_profile_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|level| level.ui_background_image_src.clone()),
|
|
|
|
|
background_music: current_profile_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|level| level.background_music.clone()),
|
|
|
|
|
board,
|
|
|
|
|
status: PuzzleRuntimeLevelStatus::Playing,
|
|
|
|
|
started_at_ms,
|
|
|
|
|
@@ -1297,6 +1322,7 @@ pub fn advance_next_level_at(
|
|
|
|
|
let next_board = build_initial_board_with_seed(next_grid_size, shuffle_seed)?;
|
|
|
|
|
let mut played_profile_ids = run.played_profile_ids.clone();
|
|
|
|
|
played_profile_ids.push(next_profile.profile_id.clone());
|
|
|
|
|
let current_profile_level = first_profile_level(next_profile);
|
|
|
|
|
|
|
|
|
|
Ok(PuzzleRunSnapshot {
|
|
|
|
|
run_id: run.run_id.clone(),
|
|
|
|
|
@@ -1309,9 +1335,8 @@ pub fn advance_next_level_at(
|
|
|
|
|
current_level: Some(PuzzleRuntimeLevelSnapshot {
|
|
|
|
|
run_id: run.run_id.clone(),
|
|
|
|
|
level_index: next_level_index,
|
|
|
|
|
level_id: next_profile
|
|
|
|
|
.levels
|
|
|
|
|
.first()
|
|
|
|
|
level_id: current_profile_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|level| level.level_id.clone()),
|
|
|
|
|
grid_size: next_grid_size,
|
|
|
|
|
profile_id: next_profile.profile_id.clone(),
|
|
|
|
|
@@ -1319,6 +1344,12 @@ pub fn advance_next_level_at(
|
|
|
|
|
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(),
|
|
|
|
|
ui_background_image_src: current_profile_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|level| level.ui_background_image_src.clone()),
|
|
|
|
|
background_music: current_profile_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|level| level.background_music.clone()),
|
|
|
|
|
board: next_board,
|
|
|
|
|
status: PuzzleRuntimeLevelStatus::Playing,
|
|
|
|
|
started_at_ms,
|
|
|
|
|
@@ -1370,6 +1401,7 @@ pub fn advance_to_new_work_first_level_at(
|
|
|
|
|
if !played_profile_ids.contains(&next_profile.profile_id) {
|
|
|
|
|
played_profile_ids.push(next_profile.profile_id.clone());
|
|
|
|
|
}
|
|
|
|
|
let current_profile_level = first_profile_level(next_profile);
|
|
|
|
|
|
|
|
|
|
Ok(PuzzleRunSnapshot {
|
|
|
|
|
run_id: run.run_id.clone(),
|
|
|
|
|
@@ -1382,9 +1414,8 @@ pub fn advance_to_new_work_first_level_at(
|
|
|
|
|
current_level: Some(PuzzleRuntimeLevelSnapshot {
|
|
|
|
|
run_id: run.run_id.clone(),
|
|
|
|
|
level_index: next_level_index,
|
|
|
|
|
level_id: next_profile
|
|
|
|
|
.levels
|
|
|
|
|
.first()
|
|
|
|
|
level_id: current_profile_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|level| level.level_id.clone()),
|
|
|
|
|
grid_size,
|
|
|
|
|
profile_id: next_profile.profile_id.clone(),
|
|
|
|
|
@@ -1392,6 +1423,12 @@ pub fn advance_to_new_work_first_level_at(
|
|
|
|
|
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(),
|
|
|
|
|
ui_background_image_src: current_profile_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|level| level.ui_background_image_src.clone()),
|
|
|
|
|
background_music: current_profile_level
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|level| level.background_music.clone()),
|
|
|
|
|
board: next_board,
|
|
|
|
|
status: PuzzleRuntimeLevelStatus::Playing,
|
|
|
|
|
started_at_ms,
|
|
|
|
|
@@ -2802,6 +2839,9 @@ mod tests {
|
|
|
|
|
level_name: format!("{profile_id} 关"),
|
|
|
|
|
picture_description: "summary".to_string(),
|
|
|
|
|
picture_reference: None,
|
|
|
|
|
ui_background_prompt: None,
|
|
|
|
|
ui_background_image_src: None,
|
|
|
|
|
ui_background_image_object_key: None,
|
|
|
|
|
background_music: None,
|
|
|
|
|
candidates: Vec::new(),
|
|
|
|
|
selected_candidate_id: None,
|
|
|
|
|
@@ -3017,6 +3057,9 @@ mod tests {
|
|
|
|
|
level_name: "第一关".to_string(),
|
|
|
|
|
picture_description: "第一关画面".to_string(),
|
|
|
|
|
picture_reference: None,
|
|
|
|
|
ui_background_prompt: None,
|
|
|
|
|
ui_background_image_src: None,
|
|
|
|
|
ui_background_image_object_key: None,
|
|
|
|
|
background_music: None,
|
|
|
|
|
candidates: Vec::new(),
|
|
|
|
|
selected_candidate_id: None,
|
|
|
|
|
@@ -3029,6 +3072,9 @@ mod tests {
|
|
|
|
|
level_name: "第二关".to_string(),
|
|
|
|
|
picture_description: "第二关画面".to_string(),
|
|
|
|
|
picture_reference: None,
|
|
|
|
|
ui_background_prompt: None,
|
|
|
|
|
ui_background_image_src: None,
|
|
|
|
|
ui_background_image_object_key: None,
|
|
|
|
|
background_music: None,
|
|
|
|
|
candidates: Vec::new(),
|
|
|
|
|
selected_candidate_id: None,
|
|
|
|
|
@@ -3072,6 +3118,30 @@ mod tests {
|
|
|
|
|
assert_eq!(next_level.time_limit_ms, 210_000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn start_run_carries_first_level_background_music() {
|
|
|
|
|
let mut profile = build_published_profile("entry", "owner-a", vec!["奇幻"]);
|
|
|
|
|
profile.levels[0].background_music = Some(PuzzleAudioAsset {
|
|
|
|
|
task_id: "suno-task-1".to_string(),
|
|
|
|
|
provider: "vector-engine-suno".to_string(),
|
|
|
|
|
asset_object_id: Some("assetobj_1".to_string()),
|
|
|
|
|
asset_kind: Some("puzzle_background_music".to_string()),
|
|
|
|
|
audio_src: "/generated-puzzle-assets/background.mp3".to_string(),
|
|
|
|
|
prompt: Some(String::new()),
|
|
|
|
|
title: Some("奇境初见".to_string()),
|
|
|
|
|
updated_at: Some("2026-05-12T00:00:00Z".to_string()),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let run = start_run("run-music".to_string(), &profile, 0).expect("run");
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
run.current_level
|
|
|
|
|
.and_then(|level| level.background_music)
|
|
|
|
|
.map(|music| music.audio_src),
|
|
|
|
|
Some("/generated-puzzle-assets/background.mp3".to_string())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn swap_pieces_marks_cleared_when_back_to_origin() {
|
|
|
|
|
let profile = build_published_profile("entry", "owner-a", vec!["蒸汽城市", "雨夜", "猫咪"]);
|
|
|
|
|
|