feat: polish jump hop themed runtime assets
This commit is contained in:
@@ -61,6 +61,9 @@ const JUMP_HOP_TILE_ATLAS_KEY_HEX: &str = "#FF00FF";
|
||||
const JUMP_HOP_BACKGROUND_IMAGE_SIZE: &str = "1024*1536";
|
||||
const JUMP_HOP_BACKGROUND_IMAGE_WIDTH: u32 = 1024;
|
||||
const JUMP_HOP_BACKGROUND_IMAGE_HEIGHT: u32 = 1536;
|
||||
const JUMP_HOP_BACK_BUTTON_IMAGE_SIZE: &str = "1024*1024";
|
||||
const JUMP_HOP_BACK_BUTTON_IMAGE_WIDTH: u32 = 1024;
|
||||
const JUMP_HOP_BACK_BUTTON_IMAGE_HEIGHT: u32 = 1024;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct JumpHopTileAtlasSlice {
|
||||
@@ -444,8 +447,12 @@ async fn maybe_generate_jump_hop_assets(
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.is_some_and(|value| !is_jump_hop_legacy_cover_composite_placeholder(value));
|
||||
let has_back_button_asset = payload
|
||||
.back_button_asset
|
||||
.as_ref()
|
||||
.is_some_and(is_jump_hop_image_asset_usable);
|
||||
|
||||
if has_complete_tile_assets && has_real_background {
|
||||
if has_complete_tile_assets && has_real_background && has_back_button_asset {
|
||||
return Ok(());
|
||||
}
|
||||
let profile_id = payload
|
||||
@@ -529,6 +536,58 @@ async fn maybe_generate_jump_hop_assets(
|
||||
payload.cover_composite = Some(background_asset.image_src);
|
||||
}
|
||||
|
||||
if !has_back_button_asset {
|
||||
let back_button_prompt = build_jump_hop_back_button_prompt(theme_text.as_str());
|
||||
let back_button_generated = create_openai_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
back_button_prompt.as_str(),
|
||||
Some(build_jump_hop_back_button_negative_prompt()),
|
||||
JUMP_HOP_BACK_BUTTON_IMAGE_SIZE,
|
||||
1,
|
||||
&[],
|
||||
"跳一跳返回按钮图生成失败",
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error)
|
||||
})?;
|
||||
let back_button_image =
|
||||
back_button_generated
|
||||
.images
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
jump_hop_error_response(
|
||||
request_context,
|
||||
JUMP_HOP_CREATION_PROVIDER,
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "跳一跳返回按钮图生成成功但未返回图片。",
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
let back_button_image =
|
||||
prepare_jump_hop_green_screen_image_for_persist(back_button_image, "跳一跳返回按钮图")
|
||||
.map_err(|error| {
|
||||
jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error)
|
||||
})?;
|
||||
let back_button_asset = persist_jump_hop_generated_image_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
profile_id.as_str(),
|
||||
"back-button",
|
||||
back_button_prompt.as_str(),
|
||||
back_button_image,
|
||||
LegacyAssetPrefix::JumpHopAssets,
|
||||
JUMP_HOP_BACK_BUTTON_IMAGE_WIDTH,
|
||||
JUMP_HOP_BACK_BUTTON_IMAGE_HEIGHT,
|
||||
request_context,
|
||||
)
|
||||
.await?;
|
||||
payload.back_button_asset = Some(back_button_asset);
|
||||
}
|
||||
|
||||
if !has_complete_tile_assets {
|
||||
let sheet_prompt =
|
||||
build_jump_hop_tile_atlas_prompt(theme_text.as_str(), tile_prompt.as_str());
|
||||
@@ -604,33 +663,110 @@ fn is_jump_hop_legacy_cover_composite_placeholder(value: &str) -> bool {
|
||||
&& (value.ends_with("/cover-composite.png") || value.contains("/cover-composite-"))
|
||||
}
|
||||
|
||||
fn build_jump_hop_background_prompt(theme_text: &str) -> String {
|
||||
fn is_jump_hop_image_asset_usable(asset: &JumpHopCharacterAsset) -> bool {
|
||||
!asset.image_src.trim().is_empty()
|
||||
&& !asset.image_object_key.trim().is_empty()
|
||||
&& !asset.asset_object_id.trim().is_empty()
|
||||
&& !asset.generation_provider.trim().is_empty()
|
||||
}
|
||||
|
||||
fn prepare_jump_hop_green_screen_image_for_persist(
|
||||
image: crate::openai_image_generation::DownloadedOpenAiImage,
|
||||
failure_label: &str,
|
||||
) -> Result<crate::openai_image_generation::DownloadedOpenAiImage, AppError> {
|
||||
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": JUMP_HOP_CREATION_PROVIDER,
|
||||
"message": format!("{failure_label}解码失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
let mut encoded = std::io::Cursor::new(Vec::new());
|
||||
crate::generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha(source)
|
||||
.write_to(&mut encoded, image::ImageFormat::Png)
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": JUMP_HOP_CREATION_PROVIDER,
|
||||
"message": format!("{failure_label}绿幕去背失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
|
||||
Ok(crate::openai_image_generation::DownloadedOpenAiImage {
|
||||
bytes: encoded.into_inner(),
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_jump_hop_generation_theme_text(theme_text: &str) -> String {
|
||||
let theme_text = theme_text.trim();
|
||||
let theme_text = if theme_text.is_empty() {
|
||||
"跳一跳"
|
||||
} else {
|
||||
theme_text
|
||||
};
|
||||
if theme_text.is_empty() {
|
||||
return "跳一跳".to_string();
|
||||
}
|
||||
|
||||
replace_jump_hop_pokemon_prompt_terms(theme_text)
|
||||
}
|
||||
|
||||
fn replace_jump_hop_pokemon_prompt_terms(value: &str) -> String {
|
||||
let mut value = value.trim().to_string();
|
||||
if value.is_empty() {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 中文注释:仅对宝可梦相关词做生成侧脱敏,避免地块图集触发上游安全拦截。
|
||||
const POKEMON_REPLACEMENTS: [(&str, &str); 15] = [
|
||||
("宝可梦", "原创幻想萌宠冒险道具"),
|
||||
("神奇宝贝", "原创幻想萌宠冒险道具"),
|
||||
("口袋妖怪", "原创幻想萌宠冒险道具"),
|
||||
("精灵球", "彩色冒险能量球"),
|
||||
("皮卡丘", "黄色闪电萌宠符号"),
|
||||
("Pokémon", "原创幻想萌宠冒险道具"),
|
||||
("Pokemon", "原创幻想萌宠冒险道具"),
|
||||
("POKEMON", "原创幻想萌宠冒险道具"),
|
||||
("pokemon", "原创幻想萌宠冒险道具"),
|
||||
("Pikachu", "黄色闪电萌宠符号"),
|
||||
("PIKACHU", "黄色闪电萌宠符号"),
|
||||
("pikachu", "黄色闪电萌宠符号"),
|
||||
("Poké Ball", "彩色冒险能量球"),
|
||||
("Poke Ball", "彩色冒险能量球"),
|
||||
("pokeball", "彩色冒险能量球"),
|
||||
];
|
||||
|
||||
for (from, to) in POKEMON_REPLACEMENTS {
|
||||
value = value.replace(from, to);
|
||||
}
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
fn build_jump_hop_background_prompt(theme_text: &str) -> String {
|
||||
let theme_text = normalize_jump_hop_generation_theme_text(theme_text);
|
||||
|
||||
format!(
|
||||
"生成一张9:16竖版跳一跳游戏背景底图,主题关键词严格只使用“{theme_text}”,不要额外改换主题;整体风格需要和同一主题的跳一跳游戏元素一致。\n画面结构必须以左右两侧氛围为主:左侧和右侧可以使用符合主题的环境元素、装饰层次、前中后景遮挡、透视节奏和行进感,让玩家感到从画面下方向上方前进。\n中央纵轴1/2区域必须是清爽的跳跃路径视觉走廊,从画面底部延伸到上方;该区域只能使用少量低对比度纹理、柔和光影、空气透视和纵深引导线,禁止堆放大型主体。\n中央纵轴1/2区域要有明显纵深感,但元素数量必须少,不能抢跳板、角色和交互层的视觉;两侧可以更有立体感、空间层次和主题氛围。\n背景只作为底图,不画任何跳板、地块、落脚物、角色、UI按钮、标题、文字、路径箭头、分数、边框、海报排版、Logo或水印。\n视角保持正面约30度的2D/2.5D休闲手游视角,相机位于场景正前方略高位置,画面有轻微向上行进的纵深,不要画成纯俯视地图、平铺俯拍、扁平壁纸或真实摄影。\n色彩清爽自然,哑光手绘质感,柔和光照,主体背景不油亮、不厚重CG、不暗黑;中央区域需要给运行态地块和陶泥儿角色留出干净可读空间。\nEnglish guardrail: vertical 9:16 mobile game background only, theme keywords strictly from \"{theme_text}\", left and right sides carry the atmosphere, the central vertical half-width corridor stays simple with sparse low-contrast details and clear depth, no platforms, no landing objects, no character, no UI, no text, consistent 2D/2.5D front-facing 30-degree game perspective."
|
||||
"生成一张9:16竖版跳一跳游戏背景底图,主题关键词严格只使用“{theme_text}”,不要额外改换主题;整体风格需要和同一主题的跳一跳游戏元素一致。\n画面结构必须以左右两侧氛围为主:左侧和右侧可以使用符合主题的环境元素、装饰层次、前中后景遮挡、透视节奏和行进感,让玩家感到从画面下方向上方前进。\n中央纵轴1/2区域必须是清爽的跳跃路径视觉走廊,从画面底部延伸到上方;该区域只能使用少量低对比度纹理、柔和光影、空气透视和纵深引导线,禁止堆放大型主体。\n中央纵轴1/2区域要有明显纵深感,但元素数量必须少,不能抢跳板、角色和交互层的视觉;两侧可以更有立体感、空间层次和主题氛围。\n背景只作为底图,不画任何跳板、地块、落脚物、角色、UI按钮、标题、文字、路径箭头、分数、边框、海报排版、Logo或水印;左上角也不要画返回按钮或任何固定图标,运行态会叠加独立可点击按钮资产。\n视角保持正面约30度的2D/2.5D休闲手游视角,相机位于场景正前方略高位置,画面有轻微向上行进的纵深,不要画成纯俯视地图、平铺俯拍、扁平壁纸或真实摄影。\n色彩清爽自然,哑光手绘质感,柔和光照,主体背景不油亮、不厚重CG、不暗黑;中央区域需要给运行态地块和陶泥儿角色留出干净可读空间。\nEnglish guardrail: vertical 9:16 mobile game background only, theme keywords strictly from \"{theme_text}\", left and right sides carry the atmosphere, the central vertical half-width corridor stays simple with sparse low-contrast details and clear depth, no platforms, no landing objects, no character, no UI, no top-left back button, no score UI, no other UI panels, consistent 2D/2.5D front-facing 30-degree game perspective."
|
||||
)
|
||||
}
|
||||
|
||||
fn build_jump_hop_background_negative_prompt() -> &'static str {
|
||||
"文字、Logo、水印、UI按钮、标题、说明文字、分数、边框、海报排版、角色、人物、跳板、地块、落脚物、平台、道路箭头、棋盘、格子、中心大型主体、中央堆满元素、中央遮挡、中央高对比装饰、中央复杂花纹、纯俯视地图、平铺俯拍、扁平壁纸、真实摄影、暗黑幻想风、厚重CG渲染、油亮高光、塑料质感"
|
||||
"文字、Logo、水印、UI按钮、返回按钮、左上角图标、右上角按钮、底部按钮、UI面板、标题、说明文字、分数、边框、海报排版、角色、人物、跳板、地块、落脚物、平台、道路箭头、棋盘、格子、中心大型主体、中央堆满元素、中央遮挡、中央高对比装饰、中央复杂花纹、纯俯视地图、平铺俯拍、扁平壁纸、真实摄影、暗黑幻想风、厚重CG渲染、油亮高光、塑料质感"
|
||||
}
|
||||
|
||||
fn build_jump_hop_back_button_prompt(theme_text: &str) -> String {
|
||||
let theme_text = normalize_jump_hop_generation_theme_text(theme_text);
|
||||
|
||||
format!(
|
||||
"生成跳一跳运行态左上角返回按钮的独立透明素材。主题关键词严格只使用“{theme_text}”,按钮的底色、材质、描边和轻微装饰跟随该主题,但必须仍然是清晰可识别的游戏 UI 返回按钮。\n按钮必须是单个标准圆形图标,圆心居中,主体视觉尺寸占画布约72%-82%,外沿有一圈干净描边,内部只有一个居中的向左箭头;不要写“返回”文字,不要数字、Logo、水印、按钮外标签或额外 UI 面板。\n允许在圆形底色里做很轻的主题材质包装,例如水果主题可用果皮色和果肉色、森林主题可用叶片色和木质描边、未来主题可用金属边和发光内环;但不要把按钮画成主题物体本身,不要继承复杂花纹、浮雕边、异形外框、贴纸堆叠或徽章装饰。\n尺寸1:1,输出绿色背景主体图,背景必须是单一纯绿色 #00FF00 且平整无纹理、无渐变、无阴影;按钮主体边缘干净,后续由服务端扣除绿色背景。按钮底色不要使用与绿幕接近的纯绿色,若主题天然包含绿色,请使用偏深、偏黄或偏蓝的主题绿色,并用高对比箭头颜色区分。\nEnglish guardrail: one standalone circular mobile game back button asset only, theme-styled colors/materials from \"{theme_text}\", centered left arrow only, no text, no logo, no extra UI, no complex badge, no object silhouette, solid #00FF00 green-screen background for later alpha removal."
|
||||
)
|
||||
}
|
||||
|
||||
fn build_jump_hop_back_button_negative_prompt() -> &'static str {
|
||||
"文字、返回文字、Logo、水印、数字、多个按钮、UI面板、海报排版、复杂徽章、花盘、浮雕边、异形外框、主题物体主体、木槌、角色、跳板、地块、落脚物、平台、透明棋盘格、白底、黑底、灰底、真实摄影、厚重CG、暗黑幻想风、油亮塑料、纯绿色按钮主体、与绿幕混在一起"
|
||||
}
|
||||
|
||||
fn build_jump_hop_tile_atlas_prompt(theme_text: &str, tile_prompt: &str) -> String {
|
||||
let theme_text = theme_text.trim();
|
||||
let theme_text = if theme_text.is_empty() {
|
||||
"跳一跳"
|
||||
} else {
|
||||
theme_text
|
||||
};
|
||||
let theme_text = normalize_jump_hop_generation_theme_text(theme_text);
|
||||
let sanitized_tile_prompt = sanitize_jump_hop_tile_prompt(tile_prompt);
|
||||
let subject_text = if sanitized_tile_prompt.is_empty() {
|
||||
theme_text
|
||||
theme_text.as_str()
|
||||
} else {
|
||||
sanitized_tile_prompt.as_str()
|
||||
};
|
||||
@@ -649,6 +785,7 @@ fn sanitize_jump_hop_tile_prompt(tile_prompt: &str) -> String {
|
||||
if value.is_empty() {
|
||||
return value;
|
||||
}
|
||||
value = replace_jump_hop_pokemon_prompt_terms(value.as_str());
|
||||
|
||||
const REPLACEMENTS: [(&str, &str); 18] = [
|
||||
("俯视角", "正面30度视角"),
|
||||
@@ -1134,6 +1271,7 @@ fn build_jump_hop_draft(payload: &JumpHopWorkspaceCreateRequest) -> JumpHopDraft
|
||||
tile_assets: Vec::new(),
|
||||
path: None,
|
||||
cover_composite: None,
|
||||
back_button_asset: None,
|
||||
generation_status: JumpHopGenerationStatus::Draft,
|
||||
}
|
||||
}
|
||||
@@ -1376,13 +1514,32 @@ mod tests {
|
||||
assert!(prompt.contains("中央纵轴1/2区域要有明显纵深感"));
|
||||
assert!(prompt.contains("两侧可以更有立体感、空间层次和主题氛围"));
|
||||
assert!(prompt.contains("不画任何跳板、地块、落脚物、角色、UI按钮"));
|
||||
assert!(prompt.contains("左上角也不要画返回按钮或任何固定图标"));
|
||||
assert!(prompt.contains("运行态会叠加独立可点击按钮资产"));
|
||||
assert!(prompt.contains("视角保持正面约30度"));
|
||||
assert!(prompt.contains("中央区域需要给运行态地块和陶泥儿角色留出干净可读空间"));
|
||||
assert!(prompt.contains("English guardrail"));
|
||||
assert!(prompt.contains("left and right sides carry the atmosphere"));
|
||||
assert!(prompt.contains("central vertical half-width corridor stays simple"));
|
||||
assert!(prompt.contains("no top-left back button"));
|
||||
assert!(prompt.contains("no platforms"));
|
||||
assert!(prompt.contains("no landing objects"));
|
||||
assert!(prompt.contains("no other UI panels"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_hop_back_button_prompt_builds_standalone_green_screen_asset() {
|
||||
let prompt = build_jump_hop_back_button_prompt("水果");
|
||||
|
||||
assert!(prompt.contains("独立透明素材"));
|
||||
assert!(prompt.contains("主题关键词严格只使用“水果”"));
|
||||
assert!(prompt.contains("单个标准圆形图标"));
|
||||
assert!(prompt.contains("内部只有一个居中的向左箭头"));
|
||||
assert!(prompt.contains("不要写“返回”文字"));
|
||||
assert!(prompt.contains("背景必须是单一纯绿色 #00FF00"));
|
||||
assert!(prompt.contains("后续由服务端扣除绿色背景"));
|
||||
assert!(prompt.contains("one standalone circular mobile game back button asset only"));
|
||||
assert!(prompt.contains("solid #00FF00 green-screen background"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1394,6 +1551,11 @@ mod tests {
|
||||
assert!(negative_prompt.contains("落脚物"));
|
||||
assert!(negative_prompt.contains("角色"));
|
||||
assert!(negative_prompt.contains("UI按钮"));
|
||||
assert!(negative_prompt.contains("返回按钮"));
|
||||
assert!(negative_prompt.contains("左上角图标"));
|
||||
assert!(negative_prompt.contains("右上角按钮"));
|
||||
assert!(negative_prompt.contains("底部按钮"));
|
||||
assert!(negative_prompt.contains("UI面板"));
|
||||
assert!(negative_prompt.contains("中央堆满元素"));
|
||||
assert!(negative_prompt.contains("中央遮挡"));
|
||||
assert!(negative_prompt.contains("纯俯视地图"));
|
||||
@@ -1416,6 +1578,34 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_hop_generation_prompt_only_rewrites_pokemon_terms() {
|
||||
let background_prompt = build_jump_hop_background_prompt("宝可梦");
|
||||
assert!(background_prompt.contains("主题关键词严格只使用“原创幻想萌宠冒险道具”"));
|
||||
assert!(!background_prompt.contains("宝可梦"));
|
||||
|
||||
let back_button_prompt = build_jump_hop_back_button_prompt("Pokemon");
|
||||
assert!(back_button_prompt.contains("主题关键词严格只使用“原创幻想萌宠冒险道具”"));
|
||||
assert!(!back_button_prompt.contains("Pokemon"));
|
||||
|
||||
let tile_prompt = build_jump_hop_tile_atlas_prompt(
|
||||
"宝可梦",
|
||||
"宝可梦主题的正面30度视角主题物体图集,包含皮卡丘和精灵球装饰",
|
||||
);
|
||||
assert!(tile_prompt.contains("主题为“原创幻想萌宠冒险道具”"));
|
||||
assert!(tile_prompt.contains("画面内容是原创幻想萌宠冒险道具主题"));
|
||||
assert!(tile_prompt.contains("黄色闪电萌宠符号"));
|
||||
assert!(tile_prompt.contains("彩色冒险能量球"));
|
||||
assert!(!tile_prompt.contains("宝可梦"));
|
||||
assert!(!tile_prompt.contains("皮卡丘"));
|
||||
assert!(!tile_prompt.contains("精灵球"));
|
||||
|
||||
let normal_prompt =
|
||||
build_jump_hop_tile_atlas_prompt("水果", "水果主题的正面30度视角主题物体图集");
|
||||
assert!(normal_prompt.contains("主题为“水果”"));
|
||||
assert!(normal_prompt.contains("画面内容是水果主题的正面30度视角主题物体图集"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_hop_tile_prompt_sanitizes_legacy_platform_words() {
|
||||
let prompt = build_jump_hop_tile_atlas_prompt(
|
||||
|
||||
@@ -121,6 +121,8 @@ pub struct JumpHopActionRequest {
|
||||
pub tile_assets: Option<Vec<JumpHopTileAsset>>,
|
||||
#[serde(default)]
|
||||
pub cover_composite: Option<String>,
|
||||
#[serde(default)]
|
||||
pub back_button_asset: Option<JumpHopCharacterAsset>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -240,6 +242,8 @@ pub struct JumpHopDraftResponse {
|
||||
pub path: Option<JumpHopPath>,
|
||||
#[serde(default)]
|
||||
pub cover_composite: Option<String>,
|
||||
#[serde(default)]
|
||||
pub back_button_asset: Option<JumpHopCharacterAsset>,
|
||||
pub generation_status: JumpHopGenerationStatus,
|
||||
}
|
||||
|
||||
@@ -308,6 +312,8 @@ pub struct JumpHopWorkProfileResponse {
|
||||
pub character_asset: JumpHopCharacterAsset,
|
||||
pub tile_atlas_asset: JumpHopCharacterAsset,
|
||||
pub tile_assets: Vec<JumpHopTileAsset>,
|
||||
#[serde(default)]
|
||||
pub back_button_asset: Option<JumpHopCharacterAsset>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
@@ -706,6 +706,14 @@ fn merge_action_into_draft(
|
||||
{
|
||||
draft.cover_composite = Some(value.to_string());
|
||||
}
|
||||
if matches!(
|
||||
scope,
|
||||
JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateTiles
|
||||
) {
|
||||
if let Some(asset) = payload.back_button_asset.clone() {
|
||||
draft.back_button_asset = Some(asset);
|
||||
}
|
||||
}
|
||||
if draft.work_title.trim().is_empty() {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop work_title 不能为空",
|
||||
@@ -763,6 +771,11 @@ fn build_compile_input(
|
||||
tile_atlas_asset_json: Some(json_string(&tile_atlas_asset)?),
|
||||
tile_assets_json: Some(json_string(&tile_assets)?),
|
||||
cover_composite,
|
||||
back_button_asset_json: draft
|
||||
.back_button_asset
|
||||
.as_ref()
|
||||
.map(json_string)
|
||||
.transpose()?,
|
||||
generation_status: Some("ready".to_string()),
|
||||
compiled_at_micros: now_micros,
|
||||
})
|
||||
@@ -848,6 +861,7 @@ fn default_draft() -> JumpHopDraftResponse {
|
||||
tile_assets: Vec::new(),
|
||||
path: None,
|
||||
cover_composite: None,
|
||||
back_button_asset: None,
|
||||
generation_status: JumpHopGenerationStatus::Draft,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ fn map_jump_hop_work_snapshot(
|
||||
.collect(),
|
||||
path: Some(map_jump_hop_path(snapshot.path.clone())),
|
||||
cover_composite: snapshot.cover_composite.clone(),
|
||||
back_button_asset: snapshot.back_button_asset.clone().map(map_character_asset),
|
||||
generation_status: parse_generation_status(&snapshot.generation_status),
|
||||
};
|
||||
let character_asset = draft
|
||||
@@ -201,6 +202,7 @@ fn map_jump_hop_work_snapshot(
|
||||
.into_iter()
|
||||
.map(map_tile_asset)
|
||||
.collect(),
|
||||
back_button_asset: snapshot.back_button_asset.map(map_character_asset),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,6 +235,7 @@ fn map_jump_hop_draft_snapshot(snapshot: JumpHopDraftSnapshot) -> JumpHopDraftRe
|
||||
.collect(),
|
||||
path: snapshot.path.map(map_jump_hop_path),
|
||||
cover_composite: snapshot.cover_composite,
|
||||
back_button_asset: snapshot.back_button_asset.map(map_character_asset),
|
||||
generation_status: parse_generation_status(&snapshot.generation_status),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ pub trait get_jump_hop_leaderboard {
|
||||
input: JumpHopLeaderboardGetInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<JumpHopLeaderboardProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
&super::ProcedureEventContext,
|
||||
Result<JumpHopLeaderboardProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ impl get_jump_hop_leaderboard for super::RemoteProcedures {
|
||||
input: JumpHopLeaderboardGetInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<JumpHopLeaderboardProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
&super::ProcedureEventContext,
|
||||
Result<JumpHopLeaderboardProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, JumpHopLeaderboardProcedureResult>(
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct JumpHopDraftCompileInput {
|
||||
pub tile_atlas_asset_json: Option<String>,
|
||||
pub tile_assets_json: Option<String>,
|
||||
pub cover_composite: Option<String>,
|
||||
pub back_button_asset_json: Option<String>,
|
||||
pub generation_status: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ pub struct JumpHopDraftSnapshot {
|
||||
pub tile_assets: Vec<JumpHopTileAssetSnapshot>,
|
||||
pub path: Option<JumpHopPath>,
|
||||
pub cover_composite: Option<String>,
|
||||
pub back_button_asset: Option<JumpHopCharacterAssetSnapshot>,
|
||||
pub generation_status: String,
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ pub struct JumpHopWorkProfileRow {
|
||||
pub published_at: Option<__sdk::Timestamp>,
|
||||
pub visible: bool,
|
||||
pub theme_text: Option<String>,
|
||||
pub back_button_asset_json: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for JumpHopWorkProfileRow {
|
||||
@@ -69,6 +70,7 @@ pub struct JumpHopWorkProfileRowCols {
|
||||
pub published_at: __sdk::__query_builder::Col<JumpHopWorkProfileRow, Option<__sdk::Timestamp>>,
|
||||
pub visible: __sdk::__query_builder::Col<JumpHopWorkProfileRow, bool>,
|
||||
pub theme_text: __sdk::__query_builder::Col<JumpHopWorkProfileRow, Option<String>>,
|
||||
pub back_button_asset_json: __sdk::__query_builder::Col<JumpHopWorkProfileRow, Option<String>>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for JumpHopWorkProfileRow {
|
||||
@@ -110,6 +112,10 @@ impl __sdk::__query_builder::HasCols for JumpHopWorkProfileRow {
|
||||
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
||||
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||
theme_text: __sdk::__query_builder::Col::new(table_name, "theme_text"),
|
||||
back_button_asset_json: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"back_button_asset_json",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ pub struct JumpHopWorkSnapshot {
|
||||
pub path: JumpHopPath,
|
||||
pub cover_image_src: String,
|
||||
pub cover_composite: Option<String>,
|
||||
pub back_button_asset: Option<JumpHopCharacterAssetSnapshot>,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
|
||||
@@ -312,6 +312,7 @@ fn create_jump_hop_agent_session_tx(
|
||||
tile_assets: Vec::new(),
|
||||
path: None,
|
||||
cover_composite: None,
|
||||
back_button_asset: None,
|
||||
generation_status: JUMP_HOP_GENERATION_DRAFT.to_string(),
|
||||
};
|
||||
ctx.db
|
||||
@@ -391,6 +392,11 @@ fn compile_jump_hop_draft_tx(
|
||||
.unwrap_or_default(),
|
||||
path: Some(path.clone()),
|
||||
cover_composite: input.cover_composite.as_deref().and_then(clean_optional),
|
||||
back_button_asset: input
|
||||
.back_button_asset_json
|
||||
.as_deref()
|
||||
.map(parse_json)
|
||||
.transpose()?,
|
||||
generation_status: input
|
||||
.generation_status
|
||||
.clone()
|
||||
@@ -425,6 +431,7 @@ fn compile_jump_hop_draft_tx(
|
||||
path_json: to_json_string(&path),
|
||||
cover_image_src: draft.cover_composite.clone().unwrap_or_default(),
|
||||
cover_composite: draft.cover_composite.clone().unwrap_or_default(),
|
||||
back_button_asset_json: draft.back_button_asset.as_ref().map(to_json_string),
|
||||
generation_status: draft.generation_status.clone(),
|
||||
publication_status: JUMP_HOP_PUBLICATION_DRAFT.to_string(),
|
||||
play_count: 0,
|
||||
@@ -830,6 +837,12 @@ fn build_work_snapshot(row: &JumpHopWorkProfileRow) -> Result<JumpHopWorkSnapsho
|
||||
path,
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
cover_composite: clean_optional(&row.cover_composite),
|
||||
back_button_asset: row
|
||||
.back_button_asset_json
|
||||
.as_deref()
|
||||
.and_then(clean_optional)
|
||||
.map(|value| parse_json(&value))
|
||||
.transpose()?,
|
||||
publication_status: row.publication_status.clone(),
|
||||
publish_ready: is_publish_ready(row),
|
||||
play_count: row.play_count,
|
||||
@@ -889,6 +902,12 @@ fn sync_session_from_work_update(
|
||||
tile_assets: parse_json_or_default(&work.tile_assets_json),
|
||||
path: Some(parse_json(&work.path_json)?),
|
||||
cover_composite: clean_optional(&work.cover_composite),
|
||||
back_button_asset: work
|
||||
.back_button_asset_json
|
||||
.as_deref()
|
||||
.and_then(clean_optional)
|
||||
.map(|value| parse_json(&value))
|
||||
.transpose()?,
|
||||
generation_status: work.generation_status.clone(),
|
||||
};
|
||||
|
||||
@@ -1209,6 +1228,11 @@ fn is_publish_ready(row: &JumpHopWorkProfileRow) -> bool {
|
||||
&& !row.tile_atlas_asset_json.trim().is_empty()
|
||||
&& !row.tile_assets_json.trim().is_empty()
|
||||
&& !row.path_json.trim().is_empty()
|
||||
&& row
|
||||
.back_button_asset_json
|
||||
.as_deref()
|
||||
.and_then(clean_optional)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn default_config_from_seed(seed_text: &str) -> JumpHopCreatorConfigSnapshot {
|
||||
@@ -1399,6 +1423,7 @@ fn clone_work(row: &JumpHopWorkProfileRow) -> JumpHopWorkProfileRow {
|
||||
published_at: row.published_at,
|
||||
visible: row.visible,
|
||||
theme_text: row.theme_text.clone(),
|
||||
back_button_asset_json: row.back_button_asset_json.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ pub struct JumpHopWorkProfileRow {
|
||||
// 跳一跳生成主题独立于作品标题;旧行按 work_title 兜底。
|
||||
#[default(None::<String>)]
|
||||
pub(crate) theme_text: Option<String>,
|
||||
// 跳一跳左上角真实可点击返回按钮的独立透明资产快照;旧行为空时运行态使用样式兜底。
|
||||
#[default(None::<String>)]
|
||||
pub(crate) back_button_asset_json: Option<String>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
|
||||
@@ -56,6 +56,7 @@ pub struct JumpHopDraftCompileInput {
|
||||
pub tile_atlas_asset_json: Option<String>,
|
||||
pub tile_assets_json: Option<String>,
|
||||
pub cover_composite: Option<String>,
|
||||
pub back_button_asset_json: Option<String>,
|
||||
pub generation_status: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
}
|
||||
@@ -248,6 +249,7 @@ pub struct JumpHopDraftSnapshot {
|
||||
pub tile_assets: Vec<JumpHopTileAssetSnapshot>,
|
||||
pub path: Option<module_jump_hop::JumpHopPath>,
|
||||
pub cover_composite: Option<String>,
|
||||
pub back_button_asset: Option<JumpHopCharacterAssetSnapshot>,
|
||||
pub generation_status: String,
|
||||
}
|
||||
|
||||
@@ -291,6 +293,7 @@ pub struct JumpHopWorkSnapshot {
|
||||
pub path: module_jump_hop::JumpHopPath,
|
||||
pub cover_image_src: String,
|
||||
pub cover_composite: Option<String>,
|
||||
pub back_button_asset: Option<JumpHopCharacterAssetSnapshot>,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
|
||||
@@ -1330,6 +1330,12 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
object
|
||||
.entry("visible".to_string())
|
||||
.or_insert_with(|| serde_json::Value::Bool(true));
|
||||
if table_name == "jump_hop_work_profile" {
|
||||
// 中文注释:跳一跳主题返回按钮资产晚于首版作品表加入,旧迁移包按未生成按钮兼容。
|
||||
object
|
||||
.entry("back_button_asset_json".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
if table_name == "match_3_d_work_profile" || table_name == "match3d_work_profile" {
|
||||
|
||||
Reference in New Issue
Block a user