This commit is contained in:
2026-05-13 00:28:07 +08:00
parent ef4f91a75e
commit 01c5ab985a
101 changed files with 10635 additions and 2292 deletions

View File

@@ -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!["蒸汽城市", "雨夜", "猫咪"]);

View File

@@ -79,6 +79,19 @@ pub struct PuzzleGeneratedImagesSaveInput {
pub saved_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleUiBackgroundSaveInput {
pub session_id: String,
pub owner_user_id: String,
pub level_id: Option<String>,
pub levels_json: Option<String>,
pub prompt: String,
pub image_src: String,
pub image_object_key: Option<String>,
pub saved_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleSelectCoverImageInput {

View File

@@ -169,6 +169,9 @@ pub fn build_puzzle_draft_from_creative_fields(
.unwrap_or_else(|| format!("{}", index + 1)),
picture_description,
picture_reference: level.picture_reference.and_then(normalize_required_string),
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,

View File

@@ -132,6 +132,12 @@ pub struct PuzzleDraftLevel {
#[serde(default)]
pub picture_reference: Option<String>,
#[serde(default)]
pub ui_background_prompt: Option<String>,
#[serde(default)]
pub ui_background_image_src: Option<String>,
#[serde(default)]
pub ui_background_image_object_key: Option<String>,
#[serde(default)]
pub background_music: Option<PuzzleAudioAsset>,
pub candidates: Vec<PuzzleGeneratedImageCandidate>,
pub selected_candidate_id: Option<String>,
@@ -356,6 +362,10 @@ pub struct PuzzleRuntimeLevelSnapshot {
pub author_display_name: String,
pub theme_tags: Vec<String>,
pub cover_image_src: Option<String>,
#[serde(default)]
pub ui_background_image_src: Option<String>,
#[serde(default)]
pub background_music: Option<PuzzleAudioAsset>,
pub board: PuzzleBoardSnapshot,
pub status: PuzzleRuntimeLevelStatus,
#[serde(default)]