1
This commit is contained in:
@@ -443,21 +443,23 @@ fn compile_match3d_draft_tx(
|
||||
let session = find_owned_session(ctx, &input.session_id, &input.owner_user_id)?;
|
||||
let config = normalize_match3d_generated_item_config(parse_config(&session.config_json)?);
|
||||
validate_config(&config)?;
|
||||
let tags = input
|
||||
.tags_json
|
||||
.as_deref()
|
||||
.map(parse_tags)
|
||||
.transpose()?
|
||||
.filter(|items| !items.is_empty())
|
||||
.unwrap_or_else(|| default_tags(&config.theme_text));
|
||||
let game_name =
|
||||
clean_optional(&input.game_name).unwrap_or_else(|| format!("{}抓大鹅", config.theme_text));
|
||||
let summary_text = input
|
||||
.summary_text
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let existing_work = ctx
|
||||
.db
|
||||
.match3d_work_profile()
|
||||
.profile_id()
|
||||
.find(&input.profile_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id);
|
||||
let tags = resolve_compile_tags(
|
||||
input.tags_json.as_deref(),
|
||||
existing_work.as_ref(),
|
||||
config.theme_text.as_str(),
|
||||
)?;
|
||||
let game_name = resolve_compile_game_name(
|
||||
&input.game_name,
|
||||
existing_work.as_ref(),
|
||||
config.theme_text.as_str(),
|
||||
);
|
||||
let summary_text = resolve_compile_summary_text(&input.summary_text, existing_work.as_ref());
|
||||
let draft = Match3DDraftSnapshot {
|
||||
profile_id: input.profile_id.clone(),
|
||||
game_name: game_name.clone(),
|
||||
@@ -468,6 +470,31 @@ fn compile_match3d_draft_tx(
|
||||
difficulty: config.difficulty,
|
||||
};
|
||||
let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros);
|
||||
let generated_item_assets_json = resolve_generated_item_assets_json_for_compile(
|
||||
input.generated_item_assets_json.as_deref(),
|
||||
existing_work.as_ref(),
|
||||
)?;
|
||||
let previous_publication_status = existing_work
|
||||
.as_ref()
|
||||
.map(|work| work.publication_status.clone())
|
||||
.unwrap_or_else(|| MATCH3D_PUBLICATION_DRAFT.to_string());
|
||||
let previous_play_count = existing_work
|
||||
.as_ref()
|
||||
.map(|work| work.play_count)
|
||||
.unwrap_or(0);
|
||||
let previous_published_at = existing_work.as_ref().and_then(|work| work.published_at);
|
||||
let cover_image_src = resolve_compile_optional_text(
|
||||
&input.cover_image_src,
|
||||
existing_work
|
||||
.as_ref()
|
||||
.map(|work| work.cover_image_src.as_str()),
|
||||
);
|
||||
let cover_asset_id = resolve_compile_optional_text(
|
||||
&input.cover_asset_id,
|
||||
existing_work
|
||||
.as_ref()
|
||||
.map(|work| work.cover_asset_id.as_str()),
|
||||
);
|
||||
let work = Match3DWorkProfileRow {
|
||||
profile_id: input.profile_id.clone(),
|
||||
owner_user_id: input.owner_user_id.clone(),
|
||||
@@ -477,18 +504,16 @@ fn compile_match3d_draft_tx(
|
||||
theme_text: config.theme_text.clone(),
|
||||
summary_text,
|
||||
tags_json: to_json_string(&tags),
|
||||
cover_image_src: clean_optional(&input.cover_image_src).unwrap_or_default(),
|
||||
cover_asset_id: clean_optional(&input.cover_asset_id).unwrap_or_default(),
|
||||
cover_image_src,
|
||||
cover_asset_id,
|
||||
clear_count: config.clear_count,
|
||||
difficulty: config.difficulty,
|
||||
config_json: to_json_string(&config),
|
||||
publication_status: MATCH3D_PUBLICATION_DRAFT.to_string(),
|
||||
play_count: 0,
|
||||
publication_status: previous_publication_status,
|
||||
play_count: previous_play_count,
|
||||
updated_at: compiled_at,
|
||||
published_at: None,
|
||||
generated_item_assets_json: normalize_generated_item_assets_json(
|
||||
input.generated_item_assets_json.as_deref(),
|
||||
)?,
|
||||
published_at: previous_published_at,
|
||||
generated_item_assets_json,
|
||||
};
|
||||
upsert_work(ctx, work);
|
||||
replace_session(
|
||||
@@ -1259,6 +1284,68 @@ fn normalize_generated_item_assets_json(value: Option<&str>) -> Result<Option<St
|
||||
Ok(Some(to_json_string(&parsed)))
|
||||
}
|
||||
|
||||
fn resolve_generated_item_assets_json_for_compile(
|
||||
input: Option<&str>,
|
||||
existing_work: Option<&Match3DWorkProfileRow>,
|
||||
) -> Result<Option<String>, String> {
|
||||
if input.is_some() {
|
||||
return normalize_generated_item_assets_json(input);
|
||||
}
|
||||
Ok(existing_work.and_then(|work| work.generated_item_assets_json.clone()))
|
||||
}
|
||||
|
||||
fn resolve_compile_tags(
|
||||
input_tags_json: Option<&str>,
|
||||
existing_work: Option<&Match3DWorkProfileRow>,
|
||||
theme_text: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
input_tags_json
|
||||
.or_else(|| existing_work.map(|work| work.tags_json.as_str()))
|
||||
.map(parse_tags)
|
||||
.transpose()
|
||||
.map(|tags| {
|
||||
tags.filter(|items| !items.is_empty())
|
||||
.unwrap_or_else(|| default_tags(theme_text))
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_compile_game_name(
|
||||
input_game_name: &Option<String>,
|
||||
existing_work: Option<&Match3DWorkProfileRow>,
|
||||
theme_text: &str,
|
||||
) -> String {
|
||||
clean_optional(input_game_name)
|
||||
.or_else(|| {
|
||||
existing_work
|
||||
.map(|work| clean_string(&work.game_name, ""))
|
||||
.filter(|value| !value.is_empty())
|
||||
})
|
||||
.unwrap_or_else(|| format!("{theme_text}抓大鹅"))
|
||||
}
|
||||
|
||||
fn resolve_compile_summary_text(
|
||||
input_summary_text: &Option<String>,
|
||||
existing_work: Option<&Match3DWorkProfileRow>,
|
||||
) -> String {
|
||||
input_summary_text
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.map(str::to_string)
|
||||
.or_else(|| existing_work.map(|work| work.summary_text.clone()))
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn resolve_compile_optional_text(input: &Option<String>, existing: Option<&str>) -> String {
|
||||
clean_optional(input)
|
||||
.or_else(|| {
|
||||
existing
|
||||
.map(|value| clean_string(value, ""))
|
||||
.filter(|value| !value.is_empty())
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn default_tags(theme_text: &str) -> Vec<String> {
|
||||
normalize_tags(vec![
|
||||
theme_text.to_string(),
|
||||
@@ -1664,6 +1751,100 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_compile_without_asset_payload_preserves_existing_generated_assets() {
|
||||
let existing = Match3DWorkProfileRow {
|
||||
profile_id: "profile-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_session_id: "session-1".to_string(),
|
||||
author_display_name: "作者".to_string(),
|
||||
game_name: "水果抓大鹅".to_string(),
|
||||
theme_text: "水果".to_string(),
|
||||
summary_text: String::new(),
|
||||
tags_json: "[\"水果\"]".to_string(),
|
||||
cover_image_src: String::new(),
|
||||
cover_asset_id: String::new(),
|
||||
clear_count: 3,
|
||||
difficulty: 3,
|
||||
config_json: to_json_string(&Match3DCreatorConfigSnapshot {
|
||||
theme_text: "水果".to_string(),
|
||||
reference_image_src: None,
|
||||
clear_count: 3,
|
||||
difficulty: 3,
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
}),
|
||||
publication_status: MATCH3D_PUBLICATION_DRAFT.to_string(),
|
||||
play_count: 2,
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
published_at: None,
|
||||
generated_item_assets_json: Some(
|
||||
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let preserved =
|
||||
resolve_generated_item_assets_json_for_compile(None, Some(&existing)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
preserved.as_deref(),
|
||||
existing.generated_item_assets_json.as_deref()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_compile_without_metadata_payload_preserves_existing_metadata() {
|
||||
let existing = Match3DWorkProfileRow {
|
||||
profile_id: "profile-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_session_id: "session-1".to_string(),
|
||||
author_display_name: "作者".to_string(),
|
||||
game_name: "果园大鹅宴".to_string(),
|
||||
theme_text: "水果".to_string(),
|
||||
summary_text: "保留描述".to_string(),
|
||||
tags_json: "[\"水果\",\"轻量休闲\"]".to_string(),
|
||||
cover_image_src: "/cover.png".to_string(),
|
||||
cover_asset_id: "cover-asset-1".to_string(),
|
||||
clear_count: 3,
|
||||
difficulty: 3,
|
||||
config_json: to_json_string(&Match3DCreatorConfigSnapshot {
|
||||
theme_text: "水果".to_string(),
|
||||
reference_image_src: None,
|
||||
clear_count: 3,
|
||||
difficulty: 3,
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
}),
|
||||
publication_status: MATCH3D_PUBLICATION_DRAFT.to_string(),
|
||||
play_count: 2,
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
published_at: None,
|
||||
generated_item_assets_json: None,
|
||||
};
|
||||
|
||||
let input_game_name = None;
|
||||
let input_summary_text = None;
|
||||
let input_tags_json = None;
|
||||
let input_cover_image_src = None;
|
||||
let input_cover_asset_id = None;
|
||||
let tags = resolve_compile_tags(input_tags_json, Some(&existing), "水果").unwrap();
|
||||
let game_name = resolve_compile_game_name(&input_game_name, Some(&existing), "水果");
|
||||
let summary_text = resolve_compile_summary_text(&input_summary_text, Some(&existing));
|
||||
let cover_image_src =
|
||||
resolve_compile_optional_text(&input_cover_image_src, Some(&existing.cover_image_src));
|
||||
let cover_asset_id =
|
||||
resolve_compile_optional_text(&input_cover_asset_id, Some(&existing.cover_asset_id));
|
||||
|
||||
assert_eq!(game_name, "果园大鹅宴");
|
||||
assert_eq!(summary_text, "保留描述");
|
||||
assert_eq!(tags, vec!["水果".to_string(), "轻量休闲".to_string()]);
|
||||
assert_eq!(cover_image_src, "/cover.png");
|
||||
assert_eq!(cover_asset_id, "cover-asset-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_compile_normalizes_clear_count_to_three_item_mvp() {
|
||||
let config = normalize_match3d_generated_item_config(Match3DCreatorConfigSnapshot {
|
||||
|
||||
@@ -31,9 +31,7 @@ pub struct CreationEntryTypeConfig {
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_creation_entry_config(
|
||||
ctx: &mut ProcedureContext,
|
||||
) -> CreationEntryConfigProcedureResult {
|
||||
pub fn get_creation_entry_config(ctx: &mut ProcedureContext) -> CreationEntryConfigProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_or_seed_creation_entry_config_snapshot(tx)) {
|
||||
Ok(record) => CreationEntryConfigProcedureResult {
|
||||
ok: true,
|
||||
@@ -180,18 +178,129 @@ fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) {
|
||||
ctx.db.creation_entry_type_config().insert(seed);
|
||||
}
|
||||
}
|
||||
|
||||
migrate_visual_novel_entry_from_old_open_default(ctx, now);
|
||||
}
|
||||
|
||||
fn migrate_visual_novel_entry_from_old_open_default(ctx: &ReducerContext, now: Timestamp) {
|
||||
let id = "visual-novel".to_string();
|
||||
let Some(row) = ctx.db.creation_entry_type_config().id().find(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// 中文注释:只纠偏旧默认种子,不覆盖后台入口开关里后续手动调整的视觉小说配置。
|
||||
let still_old_default = row.title == "视觉小说"
|
||||
&& row.subtitle == "分支叙事体验"
|
||||
&& row.badge == "可创建"
|
||||
&& row.image_src == "/creation-type-references/visual-novel.webp"
|
||||
&& row.visible
|
||||
&& row.open
|
||||
&& row.sort_order == 60;
|
||||
if !still_old_default {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.db
|
||||
.creation_entry_type_config()
|
||||
.id()
|
||||
.update(CreationEntryTypeConfig {
|
||||
badge: "敬请期待".to_string(),
|
||||
open: false,
|
||||
updated_at: now,
|
||||
..row
|
||||
});
|
||||
}
|
||||
|
||||
fn default_creation_entry_type_configs(now: Timestamp) -> Vec<CreationEntryTypeConfig> {
|
||||
vec![
|
||||
build_creation_entry_type_seed("rpg", "文字冒险", "经典 RPG 体验", "内测", "/creation-type-references/rpg.webp", false, true, 10, now),
|
||||
build_creation_entry_type_seed("big-fish", "摸鱼", "轻量闯关玩法", "可创建", "/creation-type-references/big-fish.webp", false, true, 20, now),
|
||||
build_creation_entry_type_seed("puzzle", "拼图", "拼图关卡创作", "可创建", "/creation-type-references/puzzle.webp", true, true, 30, now),
|
||||
build_creation_entry_type_seed("match3d", "抓大鹅", "3D 消除关卡", "可创建", "/creation-type-references/match3d.webp", true, true, 40, now),
|
||||
build_creation_entry_type_seed("square-hole", "方洞", "形状投放挑战", "可创建", "/creation-type-references/square-hole.webp", false, true, 50, now),
|
||||
build_creation_entry_type_seed("visual-novel", "视觉小说", "分支叙事体验", "可创建", "/creation-type-references/visual-novel.webp", true, true, 60, now),
|
||||
build_creation_entry_type_seed("airp", "AI RPG", "原生角色扮演", "即将开放", "/creation-type-references/airp.webp", true, false, 70, now),
|
||||
build_creation_entry_type_seed("creative-agent", "智能体创作", "对话式创作实验", "内测", "/creation-type-references/creative-agent.webp", false, true, 80, now),
|
||||
build_creation_entry_type_seed(
|
||||
"rpg",
|
||||
"文字冒险",
|
||||
"经典 RPG 体验",
|
||||
"内测",
|
||||
"/creation-type-references/rpg.webp",
|
||||
false,
|
||||
true,
|
||||
10,
|
||||
now,
|
||||
),
|
||||
build_creation_entry_type_seed(
|
||||
"big-fish",
|
||||
"摸鱼",
|
||||
"轻量闯关玩法",
|
||||
"可创建",
|
||||
"/creation-type-references/big-fish.webp",
|
||||
false,
|
||||
true,
|
||||
20,
|
||||
now,
|
||||
),
|
||||
build_creation_entry_type_seed(
|
||||
"puzzle",
|
||||
"拼图",
|
||||
"拼图关卡创作",
|
||||
"可创建",
|
||||
"/creation-type-references/puzzle.webp",
|
||||
true,
|
||||
true,
|
||||
30,
|
||||
now,
|
||||
),
|
||||
build_creation_entry_type_seed(
|
||||
"match3d",
|
||||
"抓大鹅",
|
||||
"3D 消除关卡",
|
||||
"可创建",
|
||||
"/creation-type-references/match3d.webp",
|
||||
true,
|
||||
true,
|
||||
40,
|
||||
now,
|
||||
),
|
||||
build_creation_entry_type_seed(
|
||||
"square-hole",
|
||||
"方洞",
|
||||
"形状投放挑战",
|
||||
"可创建",
|
||||
"/creation-type-references/square-hole.webp",
|
||||
false,
|
||||
true,
|
||||
50,
|
||||
now,
|
||||
),
|
||||
build_creation_entry_type_seed(
|
||||
"visual-novel",
|
||||
"视觉小说",
|
||||
"分支叙事体验",
|
||||
"敬请期待",
|
||||
"/creation-type-references/visual-novel.webp",
|
||||
true,
|
||||
false,
|
||||
60,
|
||||
now,
|
||||
),
|
||||
build_creation_entry_type_seed(
|
||||
"airp",
|
||||
"AI RPG",
|
||||
"原生角色扮演",
|
||||
"即将开放",
|
||||
"/creation-type-references/airp.webp",
|
||||
true,
|
||||
false,
|
||||
70,
|
||||
now,
|
||||
),
|
||||
build_creation_entry_type_seed(
|
||||
"creative-agent",
|
||||
"智能体创作",
|
||||
"对话式创作实验",
|
||||
"内测",
|
||||
"/creation-type-references/creative-agent.webp",
|
||||
false,
|
||||
true,
|
||||
80,
|
||||
now,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user