789 lines
29 KiB
Rust
789 lines
29 KiB
Rust
use crate::puzzle::{PuzzleGalleryCardViewRow, puzzle_gallery_card_view, puzzle_gallery_view};
|
|
use crate::*;
|
|
use module_custom_world::{CustomWorldGalleryEntrySnapshot, CustomWorldProfileSnapshot};
|
|
use module_puzzle::PuzzleWorkProfile;
|
|
use spacetimedb::AnonymousViewContext;
|
|
|
|
/// 跨玩法公开作品列表卡片读模型。
|
|
///
|
|
/// 该 view 只收口平台公开列表所需字段;玩法专属 runtime 配置仍留在各玩法详情 /
|
|
/// runtime procedure 中读取。
|
|
#[spacetimedb::view(accessor = public_work_gallery_entry, public)]
|
|
pub fn public_work_gallery_entry(ctx: &AnonymousViewContext) -> Vec<PublicWorkGalleryEntry> {
|
|
let mut entries = Vec::new();
|
|
|
|
entries.extend(
|
|
puzzle_gallery_card_view(ctx)
|
|
.into_iter()
|
|
.map(map_puzzle_gallery_entry),
|
|
);
|
|
entries.extend(
|
|
custom_world_public_gallery_snapshots(ctx)
|
|
.into_iter()
|
|
.map(map_custom_world_gallery_entry),
|
|
);
|
|
entries.extend(
|
|
jump_hop_gallery_card_view(ctx)
|
|
.into_iter()
|
|
.map(map_jump_hop_gallery_entry),
|
|
);
|
|
entries.extend(
|
|
wooden_fish_gallery_card_view(ctx)
|
|
.into_iter()
|
|
.map(map_wooden_fish_gallery_entry),
|
|
);
|
|
entries.extend(
|
|
match3d_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_match3d_gallery_entry),
|
|
);
|
|
entries.extend(
|
|
square_hole_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_square_hole_gallery_entry),
|
|
);
|
|
entries.extend(
|
|
visual_novel_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_visual_novel_gallery_entry),
|
|
);
|
|
entries.extend(
|
|
big_fish_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_big_fish_gallery_entry),
|
|
);
|
|
entries.extend(
|
|
bark_battle_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_bark_battle_gallery_entry),
|
|
);
|
|
|
|
sort_public_work_gallery_entries(&mut entries);
|
|
entries
|
|
}
|
|
|
|
/// 跨玩法公开作品详情摘要读模型。
|
|
///
|
|
/// `detail_payload_json` 只承载平台详情页展示扩展字段,不承载正式 runtime 配置。
|
|
#[spacetimedb::view(accessor = public_work_detail_entry, public)]
|
|
pub fn public_work_detail_entry(ctx: &AnonymousViewContext) -> Vec<PublicWorkDetailEntry> {
|
|
let mut entries = Vec::new();
|
|
|
|
entries.extend(
|
|
puzzle_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_puzzle_detail_entry),
|
|
);
|
|
entries.extend(
|
|
custom_world_public_profile_snapshots(ctx)
|
|
.into_iter()
|
|
.map(map_custom_world_detail_entry),
|
|
);
|
|
entries.extend(
|
|
jump_hop_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_jump_hop_detail_entry),
|
|
);
|
|
entries.extend(
|
|
wooden_fish_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_wooden_fish_detail_entry),
|
|
);
|
|
entries.extend(
|
|
match3d_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_match3d_detail_entry),
|
|
);
|
|
entries.extend(
|
|
square_hole_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_square_hole_detail_entry),
|
|
);
|
|
entries.extend(
|
|
visual_novel_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_visual_novel_detail_entry),
|
|
);
|
|
entries.extend(
|
|
big_fish_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_big_fish_detail_entry),
|
|
);
|
|
entries.extend(
|
|
bark_battle_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(map_bark_battle_detail_entry),
|
|
);
|
|
|
|
entries.sort_by(|left, right| {
|
|
right
|
|
.sort_time_micros
|
|
.cmp(&left.sort_time_micros)
|
|
.then_with(|| left.source_type.cmp(&right.source_type))
|
|
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
|
});
|
|
entries
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
|
pub struct PublicWorkGalleryEntry {
|
|
pub source_type: String,
|
|
pub work_id: String,
|
|
pub profile_id: String,
|
|
pub source_session_id: Option<String>,
|
|
pub public_work_code: String,
|
|
pub owner_user_id: String,
|
|
pub author_display_name: String,
|
|
pub world_name: String,
|
|
pub subtitle: String,
|
|
pub summary_text: String,
|
|
pub cover_image_src: Option<String>,
|
|
pub cover_asset_id: Option<String>,
|
|
pub theme_tags: Vec<String>,
|
|
pub play_count: u32,
|
|
pub remix_count: u32,
|
|
pub like_count: u32,
|
|
pub published_at_micros: Option<i64>,
|
|
pub updated_at_micros: i64,
|
|
pub sort_time_micros: i64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
|
pub struct PublicWorkDetailEntry {
|
|
pub source_type: String,
|
|
pub work_id: String,
|
|
pub profile_id: String,
|
|
pub source_session_id: Option<String>,
|
|
pub public_work_code: String,
|
|
pub owner_user_id: String,
|
|
pub author_display_name: String,
|
|
pub world_name: String,
|
|
pub subtitle: String,
|
|
pub summary_text: String,
|
|
pub cover_image_src: Option<String>,
|
|
pub cover_asset_id: Option<String>,
|
|
pub theme_tags: Vec<String>,
|
|
pub play_count: u32,
|
|
pub remix_count: u32,
|
|
pub like_count: u32,
|
|
pub published_at_micros: Option<i64>,
|
|
pub updated_at_micros: i64,
|
|
pub sort_time_micros: i64,
|
|
pub detail_payload_json: String,
|
|
}
|
|
|
|
fn sort_public_work_gallery_entries(entries: &mut [PublicWorkGalleryEntry]) {
|
|
entries.sort_by(|left, right| {
|
|
right
|
|
.sort_time_micros
|
|
.cmp(&left.sort_time_micros)
|
|
.then_with(|| left.source_type.cmp(&right.source_type))
|
|
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
|
});
|
|
}
|
|
|
|
fn gallery_to_detail(
|
|
entry: PublicWorkGalleryEntry,
|
|
detail_payload_json: String,
|
|
) -> PublicWorkDetailEntry {
|
|
PublicWorkDetailEntry {
|
|
source_type: entry.source_type,
|
|
work_id: entry.work_id,
|
|
profile_id: entry.profile_id,
|
|
source_session_id: entry.source_session_id,
|
|
public_work_code: entry.public_work_code,
|
|
owner_user_id: entry.owner_user_id,
|
|
author_display_name: entry.author_display_name,
|
|
world_name: entry.world_name,
|
|
subtitle: entry.subtitle,
|
|
summary_text: entry.summary_text,
|
|
cover_image_src: entry.cover_image_src,
|
|
cover_asset_id: entry.cover_asset_id,
|
|
theme_tags: entry.theme_tags,
|
|
play_count: entry.play_count,
|
|
remix_count: entry.remix_count,
|
|
like_count: entry.like_count,
|
|
published_at_micros: entry.published_at_micros,
|
|
updated_at_micros: entry.updated_at_micros,
|
|
sort_time_micros: entry.sort_time_micros,
|
|
detail_payload_json,
|
|
}
|
|
}
|
|
|
|
fn map_puzzle_gallery_entry(row: PuzzleGalleryCardViewRow) -> PublicWorkGalleryEntry {
|
|
let world_name = choose_non_empty(&[row.work_title.as_str(), row.level_name.as_str()]);
|
|
let summary_text = choose_non_empty(&[row.work_description.as_str(), row.summary.as_str()]);
|
|
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
|
|
|
|
PublicWorkGalleryEntry {
|
|
source_type: "puzzle".to_string(),
|
|
work_id: row.work_id,
|
|
profile_id: row.profile_id.clone(),
|
|
source_session_id: row.source_session_id,
|
|
public_work_code: build_prefixed_public_work_code("PZ", &row.profile_id),
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name,
|
|
subtitle: "拼图关卡".to_string(),
|
|
summary_text,
|
|
cover_image_src: row.cover_image_src,
|
|
cover_asset_id: row.cover_asset_id,
|
|
theme_tags: row.theme_tags,
|
|
play_count: row.play_count,
|
|
remix_count: row.remix_count,
|
|
like_count: row.like_count,
|
|
published_at_micros: row.published_at_micros,
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros,
|
|
}
|
|
}
|
|
|
|
fn map_puzzle_detail_entry(row: PuzzleWorkProfile) -> PublicWorkDetailEntry {
|
|
let entry = map_puzzle_gallery_entry(PuzzleGalleryCardViewRow {
|
|
work_id: row.work_id,
|
|
profile_id: row.profile_id,
|
|
owner_user_id: row.owner_user_id,
|
|
source_session_id: row.source_session_id,
|
|
author_display_name: row.author_display_name,
|
|
work_title: row.work_title,
|
|
work_description: row.work_description,
|
|
level_name: row.level_name,
|
|
summary: row.summary,
|
|
theme_tags: row.theme_tags,
|
|
cover_image_src: row.cover_image_src,
|
|
cover_asset_id: row.cover_asset_id,
|
|
publication_status: row.publication_status,
|
|
updated_at_micros: row.updated_at_micros,
|
|
published_at_micros: row.published_at_micros,
|
|
play_count: row.play_count,
|
|
remix_count: row.remix_count,
|
|
like_count: row.like_count,
|
|
point_incentive_total_half_points: row.point_incentive_total_half_points,
|
|
point_incentive_claimed_points: row.point_incentive_claimed_points,
|
|
publish_ready: row.publish_ready,
|
|
generation_status: None,
|
|
});
|
|
let detail_payload_json = json_string(json!({
|
|
"sourceType": "puzzle",
|
|
"levelCount": row.levels.len(),
|
|
"coverSlides": row.levels.iter().filter_map(|level| {
|
|
level.cover_image_src.as_ref().map(|image_src| json!({
|
|
"id": level.level_id,
|
|
"imageSrc": image_src,
|
|
"label": level.level_name,
|
|
}))
|
|
}).collect::<Vec<_>>(),
|
|
}));
|
|
gallery_to_detail(entry, detail_payload_json)
|
|
}
|
|
|
|
fn map_custom_world_gallery_entry(row: CustomWorldGalleryEntrySnapshot) -> PublicWorkGalleryEntry {
|
|
PublicWorkGalleryEntry {
|
|
source_type: "custom-world".to_string(),
|
|
work_id: format!("custom-world-work-{}", row.profile_id),
|
|
profile_id: row.profile_id,
|
|
source_session_id: None,
|
|
public_work_code: row.public_work_code,
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name: row.world_name,
|
|
subtitle: row.subtitle,
|
|
summary_text: row.summary_text,
|
|
cover_image_src: row.cover_image_src,
|
|
cover_asset_id: None,
|
|
theme_tags: vec![format_custom_world_theme_mode(row.theme_mode).to_string()],
|
|
play_count: row.play_count,
|
|
remix_count: row.remix_count,
|
|
like_count: row.like_count,
|
|
published_at_micros: Some(row.published_at_micros),
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros: row.published_at_micros,
|
|
}
|
|
}
|
|
|
|
fn map_custom_world_detail_entry(row: CustomWorldProfileSnapshot) -> PublicWorkDetailEntry {
|
|
let public_work_code = row
|
|
.public_work_code
|
|
.clone()
|
|
.unwrap_or_else(|| custom_world::build_custom_world_public_work_code(&row.profile_id));
|
|
let published_at_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
|
|
let entry = PublicWorkGalleryEntry {
|
|
source_type: "custom-world".to_string(),
|
|
work_id: format!("custom-world-work-{}", row.profile_id),
|
|
profile_id: row.profile_id,
|
|
source_session_id: row.source_agent_session_id.clone(),
|
|
public_work_code,
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name: row.world_name,
|
|
subtitle: row.subtitle,
|
|
summary_text: row.summary_text,
|
|
cover_image_src: row.cover_image_src,
|
|
cover_asset_id: None,
|
|
theme_tags: vec![format_custom_world_theme_mode(row.theme_mode).to_string()],
|
|
play_count: row.play_count,
|
|
remix_count: row.remix_count,
|
|
like_count: row.like_count,
|
|
published_at_micros: Some(published_at_micros),
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros: published_at_micros,
|
|
};
|
|
let detail_payload_json = json_string(json!({
|
|
"sourceType": "custom-world",
|
|
"authorPublicUserCode": row.author_public_user_code,
|
|
"sourceAgentSessionId": row.source_agent_session_id,
|
|
"themeMode": format_custom_world_theme_mode(row.theme_mode),
|
|
"playableNpcCount": row.playable_npc_count,
|
|
"landmarkCount": row.landmark_count,
|
|
}));
|
|
gallery_to_detail(entry, detail_payload_json)
|
|
}
|
|
|
|
fn map_jump_hop_gallery_entry(row: JumpHopGalleryCardViewRow) -> PublicWorkGalleryEntry {
|
|
let subtitle = jump_hop_difficulty_label(&row.difficulty).to_string();
|
|
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
|
|
|
|
PublicWorkGalleryEntry {
|
|
source_type: "jump-hop".to_string(),
|
|
work_id: row.work_id,
|
|
profile_id: row.profile_id,
|
|
source_session_id: None,
|
|
public_work_code: row.public_work_code,
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name: row.work_title,
|
|
subtitle,
|
|
summary_text: row.work_description,
|
|
cover_image_src: empty_string_to_option(row.cover_image_src),
|
|
cover_asset_id: None,
|
|
theme_tags: fallback_tags(row.theme_tags, &["跳一跳"]),
|
|
play_count: row.play_count,
|
|
remix_count: 0,
|
|
like_count: 0,
|
|
published_at_micros: row.published_at_micros,
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros,
|
|
}
|
|
}
|
|
|
|
fn map_jump_hop_detail_entry(row: JumpHopGalleryViewRow) -> PublicWorkDetailEntry {
|
|
let entry = PublicWorkGalleryEntry {
|
|
source_type: "jump-hop".to_string(),
|
|
work_id: row.work_id,
|
|
profile_id: row.profile_id.clone(),
|
|
source_session_id: empty_string_to_option(row.source_session_id.clone()),
|
|
public_work_code: build_prefixed_public_work_code("JH", &row.profile_id),
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name: row.work_title,
|
|
subtitle: jump_hop_difficulty_label(&row.difficulty).to_string(),
|
|
summary_text: row.work_description,
|
|
cover_image_src: empty_string_to_option(row.cover_image_src),
|
|
cover_asset_id: None,
|
|
theme_tags: fallback_tags(row.theme_tags, &["跳一跳"]),
|
|
play_count: row.play_count,
|
|
remix_count: 0,
|
|
like_count: 0,
|
|
published_at_micros: row.published_at_micros,
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros: row.published_at_micros.unwrap_or(row.updated_at_micros),
|
|
};
|
|
let detail_payload_json = json_string(json!({
|
|
"sourceType": "jump-hop",
|
|
"difficulty": row.difficulty,
|
|
"stylePreset": row.style_preset,
|
|
"tileAssetCount": row.tile_assets.len(),
|
|
"platformCount": row.path.platforms.len(),
|
|
"generationStatus": row.generation_status,
|
|
}));
|
|
gallery_to_detail(entry, detail_payload_json)
|
|
}
|
|
|
|
fn map_wooden_fish_gallery_entry(row: WoodenFishGalleryCardViewRow) -> PublicWorkGalleryEntry {
|
|
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
|
|
|
|
PublicWorkGalleryEntry {
|
|
source_type: "wooden-fish".to_string(),
|
|
work_id: row.work_id,
|
|
profile_id: row.profile_id,
|
|
source_session_id: None,
|
|
public_work_code: row.public_work_code,
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name: row.work_title,
|
|
subtitle: "敲木鱼".to_string(),
|
|
summary_text: row.work_description,
|
|
cover_image_src: empty_string_to_option(row.cover_image_src),
|
|
cover_asset_id: None,
|
|
theme_tags: fallback_tags(row.theme_tags, &["敲木鱼"]),
|
|
play_count: row.play_count,
|
|
remix_count: 0,
|
|
like_count: 0,
|
|
published_at_micros: row.published_at_micros,
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros,
|
|
}
|
|
}
|
|
|
|
fn map_wooden_fish_detail_entry(row: WoodenFishGalleryViewRow) -> PublicWorkDetailEntry {
|
|
let entry = PublicWorkGalleryEntry {
|
|
source_type: "wooden-fish".to_string(),
|
|
work_id: row.work_id,
|
|
profile_id: row.profile_id,
|
|
source_session_id: empty_string_to_option(row.source_session_id),
|
|
public_work_code: row.public_work_code,
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name: row.work_title,
|
|
subtitle: "敲木鱼".to_string(),
|
|
summary_text: row.work_description,
|
|
cover_image_src: empty_string_to_option(row.cover_image_src),
|
|
cover_asset_id: None,
|
|
theme_tags: fallback_tags(row.theme_tags, &["敲木鱼"]),
|
|
play_count: row.play_count,
|
|
remix_count: 0,
|
|
like_count: 0,
|
|
published_at_micros: row.published_at_micros,
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros: row.published_at_micros.unwrap_or(row.updated_at_micros),
|
|
};
|
|
let detail_payload_json = json_string(json!({
|
|
"sourceType": "wooden-fish",
|
|
"hitObjectPrompt": row.hit_object_prompt,
|
|
"floatingWords": row.floating_words,
|
|
"generationStatus": row.generation_status,
|
|
"hasBackgroundAsset": row.background_asset.is_some(),
|
|
"hasHitSoundAsset": row.hit_sound_asset.is_some(),
|
|
}));
|
|
gallery_to_detail(entry, detail_payload_json)
|
|
}
|
|
|
|
fn map_match3d_gallery_entry(row: Match3DGalleryViewRow) -> PublicWorkGalleryEntry {
|
|
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
|
|
|
|
PublicWorkGalleryEntry {
|
|
source_type: "match3d".to_string(),
|
|
work_id: row.profile_id.clone(),
|
|
profile_id: row.profile_id.clone(),
|
|
source_session_id: empty_string_to_option(row.source_session_id),
|
|
public_work_code: build_prefixed_public_work_code("M3", &row.profile_id),
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name: row.game_name,
|
|
subtitle: "经典消除玩法".to_string(),
|
|
summary_text: row.summary_text,
|
|
cover_image_src: empty_string_to_option(row.cover_image_src),
|
|
cover_asset_id: empty_string_to_option(row.cover_asset_id),
|
|
theme_tags: fallback_tags(row.tags, &[row.theme_text.as_str(), "抓大鹅"]),
|
|
play_count: row.play_count,
|
|
remix_count: 0,
|
|
like_count: 0,
|
|
published_at_micros: row.published_at_micros,
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros,
|
|
}
|
|
}
|
|
|
|
fn map_match3d_detail_entry(row: Match3DGalleryViewRow) -> PublicWorkDetailEntry {
|
|
let detail_payload_json = json_string(json!({
|
|
"sourceType": "match3d",
|
|
"themeText": row.theme_text,
|
|
"referenceImageSrc": row.reference_image_src,
|
|
"clearCount": row.clear_count,
|
|
"difficulty": row.difficulty,
|
|
"generatedItemAssetsReady": row.generated_item_assets_json.as_ref().is_some_and(|value| !value.trim().is_empty()),
|
|
}));
|
|
gallery_to_detail(map_match3d_gallery_entry(row), detail_payload_json)
|
|
}
|
|
|
|
fn map_square_hole_gallery_entry(row: SquareHoleGalleryViewRow) -> PublicWorkGalleryEntry {
|
|
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
|
|
|
|
PublicWorkGalleryEntry {
|
|
source_type: "square-hole".to_string(),
|
|
work_id: row.work_id,
|
|
profile_id: row.profile_id.clone(),
|
|
source_session_id: empty_string_to_option(row.source_session_id),
|
|
public_work_code: build_prefixed_public_work_code("SH", &row.profile_id),
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name: row.game_name,
|
|
subtitle: choose_non_empty(&[row.twist_rule.as_str(), "反直觉形状分拣"]),
|
|
summary_text: row.summary_text,
|
|
cover_image_src: empty_string_to_option(row.cover_image_src),
|
|
cover_asset_id: None,
|
|
theme_tags: fallback_tags(row.tags, &[row.theme_text.as_str(), "方洞挑战"]),
|
|
play_count: row.play_count,
|
|
remix_count: 0,
|
|
like_count: 0,
|
|
published_at_micros: row.published_at_micros,
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros,
|
|
}
|
|
}
|
|
|
|
fn map_square_hole_detail_entry(row: SquareHoleGalleryViewRow) -> PublicWorkDetailEntry {
|
|
let detail_payload_json = json_string(json!({
|
|
"sourceType": "square-hole",
|
|
"themeText": row.theme_text,
|
|
"twistRule": row.twist_rule,
|
|
"backgroundPrompt": row.background_prompt,
|
|
"backgroundImageSrc": empty_string_to_option(row.background_image_src.clone()),
|
|
"shapeCount": row.shape_count,
|
|
"difficulty": row.difficulty,
|
|
"shapeOptionCount": row.shape_options.len(),
|
|
"holeOptionCount": row.hole_options.len(),
|
|
}));
|
|
gallery_to_detail(map_square_hole_gallery_entry(row), detail_payload_json)
|
|
}
|
|
|
|
fn map_visual_novel_gallery_entry(row: VisualNovelGalleryViewRow) -> PublicWorkGalleryEntry {
|
|
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
|
|
|
|
PublicWorkGalleryEntry {
|
|
source_type: "visual-novel".to_string(),
|
|
work_id: row.work_id,
|
|
profile_id: row.profile_id.clone(),
|
|
source_session_id: row.source_session_id,
|
|
public_work_code: build_prefixed_public_work_code("VN", &row.profile_id),
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
world_name: row.work_title,
|
|
subtitle: "视觉小说模板".to_string(),
|
|
summary_text: row.work_description,
|
|
cover_image_src: row.cover_image_src,
|
|
cover_asset_id: None,
|
|
theme_tags: fallback_tags(row.tags, &["视觉小说"]),
|
|
play_count: row.play_count,
|
|
remix_count: 0,
|
|
like_count: 0,
|
|
published_at_micros: row.published_at_micros,
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros,
|
|
}
|
|
}
|
|
|
|
fn map_visual_novel_detail_entry(row: VisualNovelGalleryViewRow) -> PublicWorkDetailEntry {
|
|
let detail_payload_json = json_string(json!({
|
|
"sourceType": "visual-novel",
|
|
"sourceAssetIds": row.source_asset_ids,
|
|
"createdAtMicros": row.created_at_micros,
|
|
}));
|
|
gallery_to_detail(map_visual_novel_gallery_entry(row), detail_payload_json)
|
|
}
|
|
|
|
fn map_big_fish_gallery_entry(row: BigFishWorkSummarySnapshot) -> PublicWorkGalleryEntry {
|
|
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
|
|
|
|
PublicWorkGalleryEntry {
|
|
source_type: "big-fish".to_string(),
|
|
work_id: row.work_id,
|
|
profile_id: row.source_session_id.clone(),
|
|
source_session_id: Some(row.source_session_id.clone()),
|
|
public_work_code: build_prefixed_public_work_code("BF", &row.source_session_id),
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: "玩家".to_string(),
|
|
world_name: row.title,
|
|
subtitle: choose_non_empty(&[row.subtitle.as_str(), "大鱼吃小鱼"]),
|
|
summary_text: row.summary,
|
|
cover_image_src: row.cover_image_src,
|
|
cover_asset_id: None,
|
|
theme_tags: vec!["大鱼".to_string(), format!("{}级", row.level_count)],
|
|
play_count: row.play_count,
|
|
remix_count: row.remix_count,
|
|
like_count: row.like_count,
|
|
published_at_micros: row.published_at_micros,
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros,
|
|
}
|
|
}
|
|
|
|
fn map_big_fish_detail_entry(row: BigFishWorkSummarySnapshot) -> PublicWorkDetailEntry {
|
|
let detail_payload_json = json_string(json!({
|
|
"sourceType": "big-fish",
|
|
"status": row.status,
|
|
"publishReady": row.publish_ready,
|
|
"levelCount": row.level_count,
|
|
"levelMainImageReadyCount": row.level_main_image_ready_count,
|
|
"levelMotionReadyCount": row.level_motion_ready_count,
|
|
"backgroundReady": row.background_ready,
|
|
}));
|
|
gallery_to_detail(map_big_fish_gallery_entry(row), detail_payload_json)
|
|
}
|
|
|
|
fn map_bark_battle_gallery_entry(row: BarkBattleGalleryViewRow) -> PublicWorkGalleryEntry {
|
|
let cover_image_src = row
|
|
.ui_background_image_src
|
|
.clone()
|
|
.or_else(|| row.player_character_image_src.clone())
|
|
.or_else(|| row.opponent_character_image_src.clone())
|
|
.or_else(|| Some("/creation-type-references/bark-battle.webp".to_string()));
|
|
|
|
PublicWorkGalleryEntry {
|
|
source_type: "bark-battle".to_string(),
|
|
work_id: row.work_id.clone(),
|
|
profile_id: row.work_id.clone(),
|
|
source_session_id: row.source_draft_id,
|
|
public_work_code: build_bark_battle_public_work_code(&row.work_id),
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: "玩家".to_string(),
|
|
world_name: choose_non_empty(&[row.title.as_str(), "汪汪声浪大作战"]),
|
|
subtitle: format!(
|
|
"汪汪声浪 · {}",
|
|
bark_battle_difficulty_label(&row.difficulty_preset)
|
|
),
|
|
summary_text: choose_non_empty(&[
|
|
row.description.as_str(),
|
|
row.theme_description.as_str(),
|
|
"用声音能量挑战对手。",
|
|
]),
|
|
cover_image_src,
|
|
cover_asset_id: None,
|
|
theme_tags: vec![
|
|
"汪汪声浪".to_string(),
|
|
bark_battle_difficulty_label(&row.difficulty_preset).to_string(),
|
|
],
|
|
play_count: saturating_u64_to_u32(row.play_count),
|
|
remix_count: 0,
|
|
like_count: 0,
|
|
published_at_micros: Some(row.published_at_micros),
|
|
updated_at_micros: row.updated_at_micros,
|
|
sort_time_micros: row.published_at_micros,
|
|
}
|
|
}
|
|
|
|
fn map_bark_battle_detail_entry(row: BarkBattleGalleryViewRow) -> PublicWorkDetailEntry {
|
|
let detail_payload_json = json_string(json!({
|
|
"sourceType": "bark-battle",
|
|
"difficultyPreset": row.difficulty_preset,
|
|
"themeDescription": row.theme_description,
|
|
"playerImageDescription": row.player_image_description,
|
|
"opponentImageDescription": row.opponent_image_description,
|
|
"onomatopoeia": row.onomatopoeia,
|
|
"playerCharacterImageSrc": row.player_character_image_src,
|
|
"opponentCharacterImageSrc": row.opponent_character_image_src,
|
|
"uiBackgroundImageSrc": row.ui_background_image_src,
|
|
"finishCount": row.finish_count,
|
|
}));
|
|
gallery_to_detail(map_bark_battle_gallery_entry(row), detail_payload_json)
|
|
}
|
|
|
|
fn build_prefixed_public_work_code(prefix: &str, value: &str) -> String {
|
|
let normalized = normalize_public_code_text(value);
|
|
let fallback = if normalized.is_empty() {
|
|
"00000000".to_string()
|
|
} else {
|
|
normalized
|
|
};
|
|
let suffix = last_eight_padded(&fallback);
|
|
|
|
format!("{prefix}-{suffix}")
|
|
}
|
|
|
|
fn build_bark_battle_public_work_code(work_id: &str) -> String {
|
|
let normalized = normalize_public_code_text(work_id);
|
|
let without_prefix = normalized
|
|
.strip_prefix("BB")
|
|
.map(ToString::to_string)
|
|
.unwrap_or_else(|| normalized.clone());
|
|
let fallback = if without_prefix.is_empty() {
|
|
if normalized.is_empty() {
|
|
"00000000".to_string()
|
|
} else {
|
|
normalized
|
|
}
|
|
} else {
|
|
without_prefix
|
|
};
|
|
|
|
format!("BB-{}", last_eight_padded(&fallback))
|
|
}
|
|
|
|
fn normalize_public_code_text(value: &str) -> String {
|
|
value
|
|
.trim()
|
|
.chars()
|
|
.filter(|character| character.is_ascii_alphanumeric())
|
|
.flat_map(char::to_uppercase)
|
|
.collect()
|
|
}
|
|
|
|
fn last_eight_padded(value: &str) -> String {
|
|
let suffix = value
|
|
.chars()
|
|
.rev()
|
|
.take(8)
|
|
.collect::<Vec<_>>()
|
|
.into_iter()
|
|
.rev()
|
|
.collect::<String>();
|
|
format!("{suffix:0>8}")
|
|
}
|
|
|
|
fn choose_non_empty(values: &[&str]) -> String {
|
|
values
|
|
.iter()
|
|
.map(|value| value.trim())
|
|
.find(|value| !value.is_empty())
|
|
.unwrap_or_default()
|
|
.to_string()
|
|
}
|
|
|
|
fn empty_string_to_option(value: String) -> Option<String> {
|
|
let value = value.trim().to_string();
|
|
(!value.is_empty()).then_some(value)
|
|
}
|
|
|
|
fn fallback_tags(values: Vec<String>, fallback: &[&str]) -> Vec<String> {
|
|
let normalized = values
|
|
.into_iter()
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty())
|
|
.collect::<Vec<_>>();
|
|
if normalized.is_empty() {
|
|
fallback
|
|
.iter()
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty())
|
|
.collect()
|
|
} else {
|
|
normalized
|
|
}
|
|
}
|
|
|
|
fn jump_hop_difficulty_label(value: &str) -> &'static str {
|
|
match value.trim() {
|
|
"easy" => "轻松节奏",
|
|
"advanced" => "进阶跳台",
|
|
"challenge" => "极限路线",
|
|
_ => "标准路线",
|
|
}
|
|
}
|
|
|
|
fn bark_battle_difficulty_label(value: &str) -> &'static str {
|
|
match value.trim() {
|
|
"easy" => "轻松",
|
|
"hard" => "高能",
|
|
_ => "普通",
|
|
}
|
|
}
|
|
|
|
fn format_custom_world_theme_mode(value: CustomWorldThemeMode) -> &'static str {
|
|
match value {
|
|
CustomWorldThemeMode::Martial => "martial",
|
|
CustomWorldThemeMode::Arcane => "arcane",
|
|
CustomWorldThemeMode::Machina => "machina",
|
|
CustomWorldThemeMode::Tide => "tide",
|
|
CustomWorldThemeMode::Rift => "rift",
|
|
CustomWorldThemeMode::Mythic => "mythic",
|
|
}
|
|
}
|
|
|
|
fn saturating_u64_to_u32(value: u64) -> u32 {
|
|
value.min(u64::from(u32::MAX)) as u32
|
|
}
|
|
|
|
fn json_string(value: JsonValue) -> String {
|
|
serde_json::to_string(&value).unwrap_or_else(|_| "{}".to_string())
|
|
}
|