@@ -625,6 +625,9 @@ fn build_custom_world_framework_prompt(setting_text: &str) -> String {
" \" camp \" : { " . to_string ( ) ,
" \" name \" : \" 开局归处名称 \" , " . to_string ( ) ,
" \" description \" : \" 这是玩家进入世界后的第一处落脚点描述 \" , " . to_string ( ) ,
" \" sceneTaskDescription \" : \" 首次进入该场景时要生成的章节任务核心上下文 \" , " . to_string ( ) ,
" \" actBackgroundPromptTexts \" : [ \" 开局第一幕背景画面描述 \" , \" 开局第二幕背景画面描述 \" , \" 开局第三幕背景画面描述 \" ], " . to_string ( ) ,
" \" actEventDescriptions \" : [ \" 开局第一幕事件描述 \" , \" 开局第二幕事件描述 \" , \" 开局第三幕事件描述 \" ], " . to_string ( ) ,
" \" dangerLevel \" : \" low|medium|high|extreme \" " . to_string ( ) ,
" } " . to_string ( ) ,
" } " . to_string ( ) ,
@@ -635,6 +638,9 @@ fn build_custom_world_framework_prompt(setting_text: &str) -> String {
" - 这是一个完全独立的自定义世界;不要在任何正文里直接写出“武侠世界”“仙侠世界”等现成世界名。 " . to_string ( ) ,
" - templateWorldType 只是系统兼容字段,不代表正文应当引用的世界名称。 " . to_string ( ) ,
" - camp 必须表示玩家开局时的落脚处,名字不要直接写成“某某营地”,更接近归舍、住处、栖居、前哨居所这类“家/归处”的概念。 " . to_string ( ) ,
" - camp.sceneTaskDescription 必须描述玩家首次进入开局场景时要完成的核心任务,会作为游戏章节任务生成上下文,控制在 24 到 56 个汉字内。 " . to_string ( ) ,
" - camp.actBackgroundPromptTexts 必须恰好 3 条,分别对应开局场景第 1/2/3 幕背景图画面内容描述;每条都必须可直接交给生图模型,控制在 40 到 90 个汉字内。 " . to_string ( ) ,
" - camp.actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。 " . to_string ( ) ,
" - 不要输出 playableNpcs、storyNpcs、landmarks、items, 也不要输出任何角色和地图细节。 " . to_string ( ) ,
" - majorFactions 保持 2 到 3 个, coreConflicts 保持 2 到 3 个。 " . to_string ( ) ,
" - 世界设定必须直接源自玩家输入,不要脱离主题乱扩写。 " . to_string ( ) ,
@@ -650,7 +656,7 @@ fn build_custom_world_framework_json_repair_prompt(response_text: &str) -> Strin
" 顶层必须只包含: name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts、camp。 " ,
" 不要输出 playableNpcs、storyNpcs、landmarks、items 或任何其他字段。 " ,
" majorFactions 与 coreConflicts 必须是字符串数组。 " ,
" camp 必须是对象, 且包含: name、description、dangerLevel。 " ,
" camp 必须是对象, 且包含: name、description、sceneTaskDescription、actBackgroundPromptTexts、actEventDescriptions、 dangerLevel。 " ,
" 原始文本: " ,
response_text . trim ( ) ,
] . join ( " \n " )
@@ -751,7 +757,9 @@ fn build_custom_world_landmark_seed_batch_prompt(
" \" name \" : \" 场景名称 \" , " . to_string ( ) ,
" \" description \" : \" 场景极简描述 \" , " . to_string ( ) ,
" \" visualDescription \" : \" 默认场景生图描述 \" , " . to_string ( ) ,
" \" sceneTaskDescription \" : \" 首次进入该场景时要生成的章节任务核心上下文 \" , " . to_string ( ) ,
" \" actBackgroundPromptTexts \" : [ \" 第一幕背景画面描述 \" , \" 第二幕背景画面描述 \" , \" 第三幕背景画面描述 \" ], " . to_string ( ) ,
" \" actEventDescriptions \" : [ \" 第一幕事件描述 \" , \" 第二幕事件描述 \" , \" 第三幕事件描述 \" ], " . to_string ( ) ,
" \" dangerLevel \" : \" low|medium|high|extreme \" " . to_string ( ) ,
" } " . to_string ( ) ,
" ] " . to_string ( ) ,
@@ -761,10 +769,12 @@ fn build_custom_world_landmark_seed_batch_prompt(
format! ( " - 必须生成恰好 {batch_count} 个关键场景。 " ) ,
" - 这是一个完全独立的自定义世界;地点名称必须直接服务玩家输入主题。 " . to_string ( ) ,
" - 名称必须具体且互不重复,不要使用 地点1、场景1 之类的占位名。 " . to_string ( ) ,
" - 每个地点只保留: name、description、visualDescription、actBackgroundPromptTexts、dangerLevel。 " . to_string ( ) ,
" - 每个地点只保留: name、description、visualDescription、sceneTaskDescription、 actBackgroundPromptTexts、actEventDescriptions、 dangerLevel。 " . to_string ( ) ,
" - sceneTaskDescription 必须描述玩家首次进入该场景时要完成的核心任务,会作为游戏章节任务生成上下文,控制在 24 到 56 个汉字内。 " . to_string ( ) ,
" - visualDescription 是打开场景背景图像生成面板时默认填入的场景描述,必须具体到画面主体、远近景层次、地面可站立区域和氛围识别点,控制在 32 到 80 个汉字内。 " . to_string ( ) ,
" - actBackgroundPromptTexts 必须恰好 3 条,分别对应这个场景章节的第 1/2/3 幕背景图画面内容描述;每条都必须是大模型根据当前地点、主线阶段和可出场角色直接写出的画面描述,控制在 40 到 90 个汉字内。 " . to_string ( ) ,
" - actBackgroundPromptTexts 禁止使用“某某第1幕背景; 玩家会在……”这类标题、摘要、规则句拼接格式; 必须像可直接交给生图模型的自然画面描述。 " . to_string ( ) ,
" - actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。 " . to_string ( ) ,
" - description 控制在 12 到 24 个汉字内。 " . to_string ( ) ,
" - dangerLevel 只能是 low、medium、high、extreme 之一。 " . to_string ( ) ,
" - 所有生成文本都必须使用中文。 " . to_string ( ) ,
@@ -783,8 +793,8 @@ fn build_custom_world_landmark_seed_batch_json_repair_prompt(
" 顶层必须只包含一个 landmarks 数组。 " . to_string ( ) ,
format! ( " 必须保留恰好 {expected_count} 个地点对象。 " ) ,
if forbidden_names . is_empty ( ) { " " . to_string ( ) } else { format! ( " 禁止使用这些重复名: {} 。 " , forbidden_names . join ( " 、 " ) ) } ,
" 每个地点只包含: name、description、visualDescription、actBackgroundPromptTexts、dangerLevel。 " . to_string ( ) ,
" 如果缺少字段: 字符串补空字符串, actBackgroundPromptTexts 补空数组, dangerLevel 补 medium。 " . to_string ( ) ,
" 每个地点只包含: name、description、visualDescription、sceneTaskDescription、 actBackgroundPromptTexts、actEventDescriptions、 dangerLevel。 " . to_string ( ) ,
" 如果缺少字段: 字符串补空字符串, actBackgroundPromptTexts 和 actEventDescriptions 补空数组, dangerLevel 补 medium。 " . to_string ( ) ,
" 不要输出 sceneNpcNames、connectedLandmarks、items 或任何其他字段。 " . to_string ( ) ,
" 原始文本: " . to_string ( ) ,
response_text . trim ( ) . to_string ( ) ,
@@ -1061,13 +1071,14 @@ fn build_foundation_draft_profile_from_framework(
) ] )
} ) ,
) ;
object . insert ( " camp " . to_string ( ) , framework . get ( " camp " ) . cloned ( ) . unwrap_or_else ( | | json! ( { " name " : " 开局归处 " , " description " : " 玩家进入世界后的第一处落脚点。 " , " dangerLevel " : " low " } ) ) ) ;
let camp = framework . get ( " camp " ) . cloned ( ) . unwrap_or_else ( | | json! ( { " name " : " 开局归处 " , " description " : " 玩家进入世界后的第一处落脚点。 " , " dangerLevel " : " low " } ) ) ;
object . insert ( " camp " . to_string ( ) , camp . clone ( ) ) ;
object . insert (
" playableNpcs " . to_string ( ) ,
JsonValue ::Array ( playable_detailed ) ,
) ;
object . insert ( " storyNpcs " . to_string ( ) , JsonValue ::Array ( story_detailed ) ) ;
let scene_chapter_blueprints = build_scene_chapter_blueprints_from_landmarks ( & landmarks ) ;
let scene_chapter_blueprints = build_scene_chapter_blueprints_from_camp_and_ landmarks (& camp , & landmarks ) ;
object . insert ( " landmarks " . to_string ( ) , JsonValue ::Array ( landmarks ) ) ;
object . insert ( " chapters " . to_string ( ) , JsonValue ::Array ( Vec ::new ( ) ) ) ;
object . insert (
@@ -1077,45 +1088,79 @@ fn build_foundation_draft_profile_from_framework(
normalize_foundation_draft_profile ( JsonValue ::Object ( object ) , session )
}
fn build_scene_chapter_blueprints_from_camp_and_landmarks (
camp : & JsonValue ,
landmarks : & [ JsonValue ] ,
) -> Vec < JsonValue > {
let mut blueprints = Vec ::with_capacity ( landmarks . len ( ) + 1 ) ;
blueprints . push ( build_scene_chapter_blueprint_from_scene (
camp ,
0 ,
" camp " ,
" 开局归处 " ,
) ) ;
blueprints . extend ( build_scene_chapter_blueprints_from_landmarks ( landmarks ) ) ;
blueprints
}
fn build_scene_chapter_blueprints_from_landmarks ( landmarks : & [ JsonValue ] ) -> Vec < JsonValue > {
// 幕背景描述必须来自关键场景生成步骤,不能在草稿合成阶段再用规则句拼接。
landmarks
. iter ( )
. enumerate ( )
. map ( | ( chapter_index , landmark ) | {
let scene_name = json_text ( landmark , " name " )
. unwrap_or_else ( | | format! ( " 关键场景 {} " , chapter_index + 1 ) ) ;
let scene_id = json_text ( landmark , " id " )
. unwrap_or_else ( | | format! ( " saved-landmark- {} " , chapter_index + 1 ) ) ;
let summary = json_text ( landmark , " description " ) . unwrap_or_default ( ) ;
let act_prompts =
json_string_array ( landmark , " actBackgroundPromptTexts " ) . unwrap_or_default ( ) ;
let scene_npc_names = json_string_array ( landmark , " sceneNpcNames " ) . unwrap_or_default ( ) ;
json! ( {
" id " : scene_id . clone ( ) ,
" sceneId " : scene_id . clone ( ) ,
" title " : scene_name ,
" summary " : summary ,
" linkedLandmarkIds " : [ scene_id . clone ( ) ] ,
" acts " : ( 0 .. 3 )
. map ( | act_index | build_scene_act_blueprint_from_landmark (
& scene_id ,
& summary ,
& act_prompts ,
& scene_npc_names ,
act_index ,
) )
. collect ::< Vec < _ > > ( ) ,
} )
build_scene_chapter_blueprint_from_scene (
landmark ,
chapter_index ,
" saved-landmark " ,
" 关键场景 " ,
)
} )
. collect ( )
}
fn build_scene_chapter_blueprint_from_scene (
scene : & JsonValue ,
chapter_index : usize ,
id_prefix : & str ,
fallback_name_prefix : & str ,
) -> JsonValue {
let scene_name = json_text ( scene , " name " )
. unwrap_or_else ( | | format! ( " {} {} " , fallback_name_prefix , chapter_index + 1 ) ) ;
let scene_id = json_text ( scene , " id " )
. unwrap_or_else ( | | format! ( " {} - {} " , id_prefix , chapter_index + 1 ) ) ;
let summary = json_text ( scene , " description " ) . unwrap_or_default ( ) ;
let scene_task_description = json_text ( scene , " sceneTaskDescription " )
. unwrap_or_else ( | | build_default_scene_task_description ( & scene_name , & summary ) ) ;
let act_prompts = json_string_array ( scene , " actBackgroundPromptTexts " ) . unwrap_or_default ( ) ;
let act_events = json_string_array ( scene , " actEventDescriptions " ) . unwrap_or_default ( ) ;
let scene_npc_names = json_string_array ( scene , " sceneNpcNames " ) . unwrap_or_default ( ) ;
json! ( {
" id " : scene_id . clone ( ) ,
" sceneId " : scene_id . clone ( ) ,
" title " : scene_name ,
" summary " : summary ,
" sceneTaskDescription " : scene_task_description ,
" linkedLandmarkIds " : [ scene_id . clone ( ) ] ,
" acts " : ( 0 .. 3 )
. map ( | act_index | build_scene_act_blueprint_from_landmark (
& scene_id ,
& summary ,
& act_prompts ,
& act_events ,
& scene_npc_names ,
act_index ,
) )
. collect ::< Vec < _ > > ( ) ,
} )
}
fn build_scene_act_blueprint_from_landmark (
scene_id : & str ,
scene_summary : & str ,
act_prompts : & [ String ] ,
act_events : & [ String ] ,
scene_npc_names : & [ String ] ,
act_index : usize ,
) -> JsonValue {
@@ -1130,6 +1175,16 @@ fn build_scene_act_blueprint_from_landmark(
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
. unwrap_or ( " " ) ;
let opposite_npc_id = scene_npc_names . first ( ) . cloned ( ) . unwrap_or_default ( ) ;
let event_description = act_events
. get ( act_index )
. map ( String ::as_str )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
. map ( ToOwned ::to_owned )
. unwrap_or_else ( | | {
build_default_act_event_description ( scene_summary , opposite_npc_id . as_str ( ) , act_index )
} ) ;
// 缺失时保留空值,让后续生图前校验暴露底稿质量问题。
json! ( {
" id " : format ! ( " {}-act-{} " , scene_id , act_index + 1 ) ,
@@ -1138,9 +1193,42 @@ fn build_scene_act_blueprint_from_landmark(
" summary " : scene_summary ,
" backgroundPromptText " : prompt ,
" encounterNpcIds " : scene_npc_names ,
" primaryNpcId " : opposite_npc_id ,
" oppositeNpcId " : opposite_npc_id ,
" eventDescription " : event_description ,
} )
}
fn build_default_scene_task_description ( scene_name : & str , scene_summary : & str ) -> String {
if scene_summary . trim ( ) . is_empty ( ) {
return format! ( " 首次进入 {scene_name} 时,确认当前场景的核心异常、关键角色与下一步行动方向。 " ) ;
}
format! ( " 首次进入 {scene_name} 时,围绕 {scene_summary} 确认核心异常、关键角色与下一步行动方向。 " )
}
fn build_default_act_event_description (
scene_summary : & str ,
opposite_npc_id : & str ,
act_index : usize ,
) -> String {
let role_text = if opposite_npc_id . trim ( ) . is_empty ( ) {
" 当前场景关键角色 "
} else {
opposite_npc_id . trim ( )
} ;
let scene_text = if scene_summary . trim ( ) . is_empty ( ) {
" 场景内的主线压力 "
} else {
scene_summary . trim ( )
} ;
format! (
" 第 {} 幕中,玩家与 {} 正面接触,围绕 {} 处理一件会改变局势走向的事件。 " ,
act_index + 1 ,
role_text ,
scene_text ,
)
}
fn normalize_framework_shape ( framework : & mut JsonValue , setting_text : & str ) {
if ! framework . is_object ( ) {
* framework = json! ( { } ) ;
@@ -1183,6 +1271,83 @@ fn normalize_framework_shape(framework: &mut JsonValue, setting_text: &str) {
if ! object . get ( " camp " ) . is_some_and ( JsonValue ::is_object ) {
object . insert ( " camp " . to_string ( ) , json! ( { " name " : " 开局归处 " , " description " : " 玩家进入世界后的第一处落脚点。 " , " dangerLevel " : " low " } ) ) ;
}
if let Some ( camp ) = object . get_mut ( " camp " ) . and_then ( JsonValue ::as_object_mut ) {
let camp_name = camp
. get ( " name " )
. and_then ( JsonValue ::as_str )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
. unwrap_or ( " 开局归处 " )
. to_string ( ) ;
let camp_description = camp
. get ( " description " )
. and_then ( JsonValue ::as_str )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
. unwrap_or ( " 玩家进入世界后的第一处落脚点。 " )
. to_string ( ) ;
camp . insert ( " name " . to_string ( ) , JsonValue ::String ( camp_name . clone ( ) ) ) ;
camp . insert (
" description " . to_string ( ) ,
JsonValue ::String ( camp_description . clone ( ) ) ,
) ;
if ! camp
. get ( " sceneTaskDescription " )
. and_then ( JsonValue ::as_str )
. map ( str ::trim )
. is_some_and ( | value | ! value . is_empty ( ) )
{
camp . insert (
" sceneTaskDescription " . to_string ( ) ,
JsonValue ::String ( build_default_scene_task_description (
camp_name . as_str ( ) ,
camp_description . as_str ( ) ,
) ) ,
) ;
}
if ! camp
. get ( " actBackgroundPromptTexts " )
. and_then ( JsonValue ::as_array )
. is_some_and ( | items | items . len ( ) = = 3 )
{
// 中文注释:开局场景也必须进入逐幕生图队列;模型漏字段时用 camp 信息生成可用的三幕画面描述。
camp . insert (
" actBackgroundPromptTexts " . to_string ( ) ,
JsonValue ::Array (
( 0 .. 3 )
. map ( | index | {
JsonValue ::String ( format! (
" {} 第 {} 幕, {} ,画面保留玩家站位、近景可交互物件与远景世界压力。 " ,
camp_name ,
index + 1 ,
camp_description ,
) )
} )
. collect ( ) ,
) ,
) ;
}
if ! camp
. get ( " actEventDescriptions " )
. and_then ( JsonValue ::as_array )
. is_some_and ( | items | items . len ( ) = = 3 )
{
camp . insert (
" actEventDescriptions " . to_string ( ) ,
JsonValue ::Array (
( 0 .. 3 )
. map ( | index | {
JsonValue ::String ( build_default_act_event_description (
camp_description . as_str ( ) ,
" 开局关键角色 " ,
index ,
) )
} )
. collect ( ) ,
) ,
) ;
}
}
}
fn build_framework_summary_text ( framework : & JsonValue , max_landmarks : usize ) -> String {
@@ -1516,6 +1681,25 @@ fn normalize_scene_chapter_blueprint(chapter: JsonValue) -> JsonValue {
. filter ( | value | ! value . is_empty ( ) )
. unwrap_or ( " 第一幕 " ) ;
object . insert ( " title " . to_string ( ) , JsonValue ::String ( title . to_string ( ) ) ) ;
let summary = object
. get ( " summary " )
. and_then ( JsonValue ::as_str )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
. map ( ToOwned ::to_owned )
. unwrap_or_else ( | | " 第一幕用于让玩家进入当前世界的主线矛盾。 " . to_string ( ) ) ;
object . insert ( " summary " . to_string ( ) , JsonValue ::String ( summary . clone ( ) ) ) ;
let scene_task_description = object
. get ( " sceneTaskDescription " )
. and_then ( JsonValue ::as_str )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
. map ( ToOwned ::to_owned )
. unwrap_or_else ( | | build_default_scene_task_description ( title , summary . as_str ( ) ) ) ;
object . insert (
" sceneTaskDescription " . to_string ( ) ,
JsonValue ::String ( scene_task_description ) ,
) ;
let acts = object
. get ( " acts " )
. and_then ( JsonValue ::as_array )
@@ -1569,6 +1753,57 @@ fn normalize_scene_act_blueprint(act: JsonValue, index: usize) -> JsonValue {
" backgroundPromptText " . to_string ( ) ,
JsonValue ::String ( background_prompt ) ,
) ;
let encounter_npc_ids = object
. get ( " encounterNpcIds " )
. and_then ( JsonValue ::as_array )
. map ( | items | {
items
. iter ( )
. filter_map ( | entry | entry . as_str ( ) . map ( str ::trim ) )
. filter ( | value | ! value . is_empty ( ) )
. map ( | value | JsonValue ::String ( value . to_string ( ) ) )
. collect ::< Vec < _ > > ( )
} )
. unwrap_or_default ( ) ;
let opposite_npc_id = object
. get ( " oppositeNpcId " )
. and_then ( JsonValue ::as_str )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
. or_else ( | | {
object
. get ( " primaryNpcId " )
. and_then ( JsonValue ::as_str )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
} )
. map ( ToOwned ::to_owned )
. or_else ( | | {
encounter_npc_ids
. first ( )
. and_then ( JsonValue ::as_str )
. map ( ToOwned ::to_owned )
} )
. unwrap_or_default ( ) ;
let event_description = object
. get ( " eventDescription " )
. and_then ( JsonValue ::as_str )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
. map ( ToOwned ::to_owned )
. unwrap_or_else ( | | {
build_default_act_event_description ( summary . as_str ( ) , opposite_npc_id . as_str ( ) , index )
} ) ;
object . insert ( " encounterNpcIds " . to_string ( ) , JsonValue ::Array ( encounter_npc_ids ) ) ;
object . insert (
" primaryNpcId " . to_string ( ) ,
JsonValue ::String ( opposite_npc_id . clone ( ) ) ,
) ;
object . insert ( " oppositeNpcId " . to_string ( ) , JsonValue ::String ( opposite_npc_id ) ) ;
object . insert (
" eventDescription " . to_string ( ) ,
JsonValue ::String ( event_description ) ,
) ;
JsonValue ::Object ( object )
}
@@ -1577,6 +1812,7 @@ fn build_fallback_scene_chapter_blueprint() -> JsonValue {
" id " : " chapter-act-1 " ,
" title " : " 第一幕 " ,
" summary " : " 第一幕用于让玩家进入当前世界的主线矛盾,并看见最初的风险与方向。 " ,
" sceneTaskDescription " : " 首次进入当前场景时,确认主线矛盾、关键角色与下一步追查方向。 " ,
" acts " : [ build_fallback_scene_act ( ) ] ,
} )
}
@@ -1591,6 +1827,10 @@ fn build_fallback_scene_act_with_index(index: usize) -> JsonValue {
" title " : if index = = 0 { " 开场场景幕 " . to_string ( ) } else { format! ( " 第 {} 幕 " , index + 1 ) } ,
" summary " : " 玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。 " ,
" backgroundPromptText " : " " ,
" encounterNpcIds " : [ ] ,
" primaryNpcId " : " " ,
" oppositeNpcId " : " " ,
" eventDescription " : build_default_act_event_description ( " 玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。 " , " " , index ) ,
} )
}
@@ -1769,6 +2009,42 @@ mod tests {
) ;
}
#[ test ]
fn scene_chapter_blueprints_include_opening_camp_acts ( ) {
let mut framework = json! ( {
" camp " : {
" name " : " 萧家祖宅 " ,
" description " : " 玩家开局并成长的家族祖宅。 " ,
" dangerLevel " : " low "
}
} ) ;
normalize_framework_shape ( & mut framework , " 废柴少年的逆袭传奇 " ) ;
let camp = framework
. get ( " camp " )
. expect ( " camp should exist after normalize " ) ;
let landmarks = vec! [ json! ( {
" id " : " landmark-duel-ground " ,
" name " : " 萧家斗技场 " ,
" description " : " 萧家子弟修炼斗技、比试的场所。 " ,
" actBackgroundPromptTexts " : [ " 斗技台晨雾未散,石阶旁少年们列队观望。 " , " 木桩与兵器架围出训练区,族徽旗帜在风里猎猎。 " , " 暮色压下斗技场,中央擂台留出一对一交锋空间。 " ]
} ) ] ;
let blueprints = build_scene_chapter_blueprints_from_camp_and_landmarks ( camp , & landmarks ) ;
let opening_chapter = & blueprints [ 0 ] ;
let opening_acts = opening_chapter
. get ( " acts " )
. and_then ( JsonValue ::as_array )
. expect ( " opening camp acts should exist " ) ;
assert_eq! ( opening_chapter . get ( " sceneId " ) , Some ( & json! ( " camp-1 " ) ) ) ;
assert_eq! ( opening_acts . len ( ) , 3 ) ;
assert! ( opening_acts . iter ( ) . all ( | act | act
. get ( " backgroundPromptText " )
. and_then ( JsonValue ::as_str )
. is_some_and ( | value | ! value . trim ( ) . is_empty ( ) ) ) ) ;
assert_eq! ( blueprints . len ( ) , 2 ) ;
}
#[ test ]
fn normalize_scene_act_keeps_missing_background_prompt_empty ( ) {
let act = normalize_scene_act_blueprint (
@@ -1943,6 +2219,15 @@ mod tests {
. and_then ( JsonValue ::as_str )
. is_some ( )
) ;
assert_eq! (
draft_profile
. get ( " sceneChapterBlueprints " )
. and_then ( JsonValue ::as_array )
. and_then ( | entries | entries . first ( ) )
. and_then ( | entry | entry . get ( " sceneId " ) )
. and_then ( JsonValue ::as_str ) ,
Some ( " camp-1 " )
) ;
assert_eq! (
draft_profile
. get ( " sceneChapterBlueprints " )
@@ -1950,8 +2235,8 @@ mod tests {
. and_then ( | entries | entries . first ( ) )
. and_then ( | entry | entry . get ( " acts " ) )
. and_then ( JsonValue ::as_array )
. map ( | entries | ! entries . is_empty ( ) ) ,
Some ( true )
. map ( Vec ::len ) ,
Some ( 3 )
) ;
}