Update Match3D/image-generation docs & code
Adds/updates documentation, assets and implementation for Match3D and puzzle image generation workflows. Key changes: decision logs and pitfalls updated to prefer VectorEngine Gemini for Match3D material sheets and to require edits (multipart) for 1:1 container reference images; guidance added for when to use APIMart vs VectorEngine. .env.example clarified APIMart/Responses config. Many new public assets and PPT visuals added. Code changes across frontend and backend: updated shared contracts, server-rs match3d/puzzle/image-generation handlers, VectorEngine/OpenAI image generation clients, and multiple React components/tests to handle UI/background/container image signing, edits workflow, and puzzle UI background resolution. Added src/services/puzzle-runtime/puzzleUiBackgroundSource.ts and related test updates. Includes notes about multipart HTTP/1.1 requirement and test/verification commands in docs.
This commit is contained in:
@@ -1070,6 +1070,9 @@ pub fn start_run_with_shuffle_seed_at(
|
||||
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()),
|
||||
background_music: current_profile_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.background_music.clone()),
|
||||
@@ -1347,6 +1350,9 @@ pub fn advance_next_level_at(
|
||||
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()),
|
||||
background_music: current_profile_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.background_music.clone()),
|
||||
@@ -1426,6 +1432,9 @@ pub fn advance_to_new_work_first_level_at(
|
||||
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()),
|
||||
background_music: current_profile_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.background_music.clone()),
|
||||
@@ -3131,14 +3140,50 @@ mod tests {
|
||||
title: Some("奇境初见".to_string()),
|
||||
updated_at: Some("2026-05-12T00:00:00Z".to_string()),
|
||||
});
|
||||
profile.levels[0].ui_background_image_object_key =
|
||||
Some("generated-puzzle-assets/background-ui.png".to_string());
|
||||
|
||||
let run = start_run("run-music".to_string(), &profile, 0).expect("run");
|
||||
let current_level = run.current_level.as_ref().expect("level");
|
||||
|
||||
assert_eq!(
|
||||
run.current_level
|
||||
.and_then(|level| level.background_music)
|
||||
.map(|music| music.audio_src),
|
||||
current_level
|
||||
.background_music
|
||||
.as_ref()
|
||||
.map(|music| music.audio_src.as_str()),
|
||||
Some("/generated-puzzle-assets/background.mp3".to_string())
|
||||
.as_deref()
|
||||
);
|
||||
assert_eq!(
|
||||
current_level.ui_background_image_object_key.as_deref(),
|
||||
Some("generated-puzzle-assets/background-ui.png")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advance_to_new_work_first_level_carries_ui_background_object_key() {
|
||||
let first_profile = build_published_profile("entry", "owner-a", vec!["奇幻"]);
|
||||
let mut next_profile = build_published_profile("next", "owner-b", vec!["奇幻"]);
|
||||
next_profile.levels[0].ui_background_image_object_key =
|
||||
Some("generated-puzzle-assets/next-ui.png".to_string());
|
||||
|
||||
let run = start_run("run-ui".to_string(), &first_profile, 2).expect("run");
|
||||
let mut cleared_run = run.clone();
|
||||
cleared_run.cleared_level_count = cleared_run.current_level_index;
|
||||
let current_level = cleared_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_run =
|
||||
advance_to_new_work_first_level_at(&cleared_run, &next_profile, 3_000).expect("next run");
|
||||
|
||||
assert_eq!(
|
||||
next_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.and_then(|level| level.ui_background_image_object_key.as_deref()),
|
||||
Some("generated-puzzle-assets/next-ui.png")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -365,6 +365,8 @@ pub struct PuzzleRuntimeLevelSnapshot {
|
||||
#[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 board: PuzzleBoardSnapshot,
|
||||
pub status: PuzzleRuntimeLevelStatus,
|
||||
|
||||
Reference in New Issue
Block a user