Add generationStatus and match3d/runtime fixes
Introduce persistent generationStatus to work summaries (puzzle & match3d) and propagate generation recovery rules across docs and frontend/backends so "generating" is restored from server-side work summary rather than ephemeral front-end notices. Update API server image/asset handling (improve match3d material sheet green/alpha decontamination and promote generatedItemAssets background fields) and add runtime improvements: alpha-based hotspot hit-testing, tray insertion/three-match animation behavior, and session re-read on client-side VectorEngine timeouts/lock-screen interruptions. Many docs, tests and related frontend modules updated/added to reflect these contract and behavior changes.
This commit is contained in:
@@ -786,6 +786,45 @@ fn first_profile_level(profile: &PuzzleWorkProfile) -> Option<PuzzleDraftLevel>
|
||||
.next()
|
||||
}
|
||||
|
||||
fn first_profile_ui_background_level(profile: &PuzzleWorkProfile) -> Option<PuzzleDraftLevel> {
|
||||
normalize_puzzle_levels(profile.levels.clone(), &profile.theme_tags)
|
||||
.unwrap_or_else(|_| profile.levels.clone())
|
||||
.into_iter()
|
||||
.find(|level| {
|
||||
level
|
||||
.ui_background_image_src
|
||||
.as_deref()
|
||||
.and_then(normalize_required_string)
|
||||
.is_some()
|
||||
|| level
|
||||
.ui_background_image_object_key
|
||||
.as_deref()
|
||||
.and_then(normalize_required_string)
|
||||
.is_some()
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_puzzle_runtime_ui_background_fields(
|
||||
level: Option<&PuzzleDraftLevel>,
|
||||
fallback_level: Option<&PuzzleDraftLevel>,
|
||||
) -> (Option<String>, Option<String>) {
|
||||
for candidate in [level, fallback_level].into_iter().flatten() {
|
||||
let image_src = candidate
|
||||
.ui_background_image_src
|
||||
.as_deref()
|
||||
.and_then(normalize_required_string);
|
||||
let object_key = candidate
|
||||
.ui_background_image_object_key
|
||||
.as_deref()
|
||||
.and_then(|value| normalize_required_string(value.trim_start_matches('/')));
|
||||
if image_src.is_some() || object_key.is_some() {
|
||||
return (image_src, object_key);
|
||||
}
|
||||
}
|
||||
|
||||
(None, None)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -1047,6 +1086,12 @@ pub fn start_run_with_shuffle_seed_at(
|
||||
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);
|
||||
let ui_background_level = first_profile_ui_background_level(entry_profile);
|
||||
let (ui_background_image_src, ui_background_image_object_key) =
|
||||
resolve_puzzle_runtime_ui_background_fields(
|
||||
current_profile_level.as_ref(),
|
||||
ui_background_level.as_ref(),
|
||||
);
|
||||
Ok(PuzzleRunSnapshot {
|
||||
run_id: run_id.clone(),
|
||||
entry_profile_id: entry_profile.profile_id.clone(),
|
||||
@@ -1067,12 +1112,8 @@ 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()),
|
||||
ui_background_image_object_key: current_profile_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.ui_background_image_object_key.clone()),
|
||||
ui_background_image_src,
|
||||
ui_background_image_object_key,
|
||||
background_music: current_profile_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.background_music.clone()),
|
||||
@@ -1326,6 +1367,16 @@ pub fn advance_next_level_at(
|
||||
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);
|
||||
let ui_background_level = first_profile_ui_background_level(next_profile);
|
||||
let (mut ui_background_image_src, mut ui_background_image_object_key) =
|
||||
resolve_puzzle_runtime_ui_background_fields(
|
||||
current_profile_level.as_ref(),
|
||||
ui_background_level.as_ref(),
|
||||
);
|
||||
if ui_background_image_src.is_none() && ui_background_image_object_key.is_none() {
|
||||
ui_background_image_src = current_level.ui_background_image_src.clone();
|
||||
ui_background_image_object_key = current_level.ui_background_image_object_key.clone();
|
||||
}
|
||||
|
||||
Ok(PuzzleRunSnapshot {
|
||||
run_id: run.run_id.clone(),
|
||||
@@ -1347,12 +1398,8 @@ 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()),
|
||||
ui_background_image_object_key: current_profile_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.ui_background_image_object_key.clone()),
|
||||
ui_background_image_src,
|
||||
ui_background_image_object_key,
|
||||
background_music: current_profile_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.background_music.clone()),
|
||||
@@ -1408,6 +1455,12 @@ pub fn advance_to_new_work_first_level_at(
|
||||
played_profile_ids.push(next_profile.profile_id.clone());
|
||||
}
|
||||
let current_profile_level = first_profile_level(next_profile);
|
||||
let ui_background_level = first_profile_ui_background_level(next_profile);
|
||||
let (ui_background_image_src, ui_background_image_object_key) =
|
||||
resolve_puzzle_runtime_ui_background_fields(
|
||||
current_profile_level.as_ref(),
|
||||
ui_background_level.as_ref(),
|
||||
);
|
||||
|
||||
Ok(PuzzleRunSnapshot {
|
||||
run_id: run.run_id.clone(),
|
||||
@@ -1429,12 +1482,8 @@ 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()),
|
||||
ui_background_image_object_key: current_profile_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.ui_background_image_object_key.clone()),
|
||||
ui_background_image_src,
|
||||
ui_background_image_object_key,
|
||||
background_music: current_profile_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.background_music.clone()),
|
||||
@@ -3151,8 +3200,7 @@ mod tests {
|
||||
.background_music
|
||||
.as_ref()
|
||||
.map(|music| music.audio_src.as_str()),
|
||||
Some("/generated-puzzle-assets/background.mp3".to_string())
|
||||
.as_deref()
|
||||
Some("/generated-puzzle-assets/background.mp3".to_string()).as_deref()
|
||||
);
|
||||
assert_eq!(
|
||||
current_level.ui_background_image_object_key.as_deref(),
|
||||
@@ -3175,8 +3223,8 @@ mod tests {
|
||||
current_level.cleared_at_ms = Some(2_000);
|
||||
current_level.elapsed_ms = Some(1_000);
|
||||
|
||||
let next_run =
|
||||
advance_to_new_work_first_level_at(&cleared_run, &next_profile, 3_000).expect("next run");
|
||||
let next_run = advance_to_new_work_first_level_at(&cleared_run, &next_profile, 3_000)
|
||||
.expect("next run");
|
||||
|
||||
assert_eq!(
|
||||
next_run
|
||||
@@ -3187,6 +3235,52 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_work_next_level_inherits_first_available_ui_background() {
|
||||
let mut profile = build_published_profile("entry", "owner-a", vec!["奇幻"]);
|
||||
profile.levels[0].ui_background_image_src =
|
||||
Some("/generated-puzzle-assets/entry-ui.png".to_string());
|
||||
profile.levels.push(PuzzleDraftLevel {
|
||||
level_id: "puzzle-level-2".to_string(),
|
||||
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,
|
||||
cover_image_src: Some("/level-2.png".to_string()),
|
||||
cover_asset_id: None,
|
||||
generation_status: "ready".to_string(),
|
||||
});
|
||||
|
||||
let mut run = start_run("run-same-work-ui".to_string(), &profile, 0).expect("run");
|
||||
run.cleared_level_count = run.current_level_index;
|
||||
let current_level = run.current_level.as_mut().expect("level");
|
||||
current_level.status = PuzzleRuntimeLevelStatus::Cleared;
|
||||
current_level.cleared_at_ms = Some(2_000);
|
||||
current_level.elapsed_ms = Some(1_000);
|
||||
let next_level = selected_profile_level_after_runtime_level(&profile, current_level)
|
||||
.expect("same work next level");
|
||||
let mut next_profile = profile.clone();
|
||||
next_profile.level_name = next_level.level_name.clone();
|
||||
next_profile.cover_image_src = next_level.cover_image_src.clone();
|
||||
next_profile.cover_asset_id = next_level.cover_asset_id.clone();
|
||||
next_profile.levels = vec![next_level];
|
||||
|
||||
let next_run = advance_next_level_at(&run, &next_profile, 3_000).expect("next run");
|
||||
|
||||
assert_eq!(
|
||||
next_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.ui_background_image_src.as_deref()),
|
||||
Some("/generated-puzzle-assets/entry-ui.png")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_pieces_marks_cleared_when_back_to_origin() {
|
||||
let profile = build_published_profile("entry", "owner-a", vec!["蒸汽城市", "雨夜", "猫咪"]);
|
||||
|
||||
Reference in New Issue
Block a user