1
This commit is contained in:
@@ -75,7 +75,7 @@ pub async fn generate_custom_world_foundation_draft(
|
||||
.await?;
|
||||
framework["storyNpcs"] = JsonValue::Array(story_outlines.clone());
|
||||
|
||||
let landmarks = generate_foundation_landmark_seed_entries(
|
||||
let generated_scene_entries = generate_foundation_landmark_seed_entries(
|
||||
llm_client,
|
||||
&framework,
|
||||
FOUNDATION_DRAFT_LANDMARK_COUNT,
|
||||
@@ -83,7 +83,7 @@ pub async fn generate_custom_world_foundation_draft(
|
||||
&mut on_progress,
|
||||
)
|
||||
.await?;
|
||||
framework["landmarks"] = JsonValue::Array(landmarks.clone());
|
||||
framework["landmarks"] = JsonValue::Array(generated_scene_entries.clone());
|
||||
|
||||
let playable_narrative = expand_foundation_role_entries(
|
||||
llm_client,
|
||||
@@ -137,7 +137,7 @@ pub async fn generate_custom_world_foundation_draft(
|
||||
framework,
|
||||
playable_detailed,
|
||||
story_detailed,
|
||||
landmarks,
|
||||
generated_scene_entries,
|
||||
session,
|
||||
setting_text.as_str(),
|
||||
);
|
||||
@@ -304,6 +304,7 @@ async fn generate_foundation_landmark_seed_entries(
|
||||
}
|
||||
let batch_count = (total_count - merged_entries.len()).min(FOUNDATION_LANDMARK_BATCH_SIZE);
|
||||
let forbidden_names = names_from_entries(&merged_entries);
|
||||
let is_opening_batch = batch_index == 0 && merged_entries.is_empty();
|
||||
emit_foundation_draft_progress(
|
||||
on_progress,
|
||||
"生成关键场景",
|
||||
@@ -319,13 +320,19 @@ async fn generate_foundation_landmark_seed_entries(
|
||||
);
|
||||
let raw = request_foundation_json_stage(
|
||||
llm_client,
|
||||
build_custom_world_landmark_seed_batch_prompt(framework, batch_count, &forbidden_names),
|
||||
build_custom_world_landmark_seed_batch_prompt(
|
||||
framework,
|
||||
batch_count,
|
||||
&forbidden_names,
|
||||
is_opening_batch,
|
||||
),
|
||||
format!("agent-foundation-landmark-seed-batch-{}", batch_index + 1).as_str(),
|
||||
|response_text| {
|
||||
build_custom_world_landmark_seed_batch_json_repair_prompt(
|
||||
response_text,
|
||||
batch_count,
|
||||
&forbidden_names,
|
||||
is_opening_batch,
|
||||
)
|
||||
},
|
||||
format!(
|
||||
@@ -714,7 +721,7 @@ fn build_foundation_draft_profile_from_framework(
|
||||
framework: JsonValue,
|
||||
playable_detailed: Vec<JsonValue>,
|
||||
story_detailed: Vec<JsonValue>,
|
||||
landmarks: Vec<JsonValue>,
|
||||
generated_scene_entries: Vec<JsonValue>,
|
||||
session: &CustomWorldAgentSessionRecord,
|
||||
setting_text: &str,
|
||||
) -> JsonMap<String, JsonValue> {
|
||||
@@ -793,9 +800,14 @@ fn build_foundation_draft_profile_from_framework(
|
||||
setting_text,
|
||||
),
|
||||
);
|
||||
let camp = framework.get("camp").cloned().unwrap_or_else(
|
||||
let fallback_camp = framework.get("camp").cloned().unwrap_or_else(
|
||||
|| json!({ "name": "开局归处", "description": "玩家进入世界后的第一处落脚点。" }),
|
||||
);
|
||||
let playable_detailed = assign_role_ids(playable_detailed, "playable-npc");
|
||||
let story_detailed = assign_role_ids(story_detailed, "story-npc");
|
||||
let scene_role_refs = collect_scene_role_refs(&story_detailed);
|
||||
let (camp, landmarks) =
|
||||
split_generated_scenes_into_camp_and_landmarks(fallback_camp, generated_scene_entries);
|
||||
object.insert("camp".to_string(), camp.clone());
|
||||
object.insert(
|
||||
"playableNpcs".to_string(),
|
||||
@@ -803,7 +815,7 @@ fn build_foundation_draft_profile_from_framework(
|
||||
);
|
||||
object.insert("storyNpcs".to_string(), JsonValue::Array(story_detailed));
|
||||
let scene_chapter_blueprints =
|
||||
build_scene_chapter_blueprints_from_camp_and_landmarks(&camp, &landmarks);
|
||||
build_scene_chapter_blueprints_from_camp_and_landmarks(&camp, &landmarks, &scene_role_refs);
|
||||
object.insert("landmarks".to_string(), JsonValue::Array(landmarks));
|
||||
object.insert("chapters".to_string(), JsonValue::Array(Vec::new()));
|
||||
object.insert(
|
||||
@@ -1095,9 +1107,50 @@ fn stable_ascii_slug(value: &str) -> String {
|
||||
format!("{hash:08x}")
|
||||
}
|
||||
|
||||
fn split_generated_scenes_into_camp_and_landmarks(
|
||||
fallback_camp: JsonValue,
|
||||
generated_scene_entries: Vec<JsonValue>,
|
||||
) -> (JsonValue, Vec<JsonValue>) {
|
||||
let mut entries = generated_scene_entries.into_iter();
|
||||
let opening_scene = entries.next().unwrap_or(fallback_camp);
|
||||
let camp = normalize_generated_opening_scene(opening_scene);
|
||||
let landmarks = entries.collect::<Vec<_>>();
|
||||
(camp, landmarks)
|
||||
}
|
||||
|
||||
fn normalize_generated_opening_scene(scene: JsonValue) -> JsonValue {
|
||||
let mut object = scene.as_object().cloned().unwrap_or_default();
|
||||
let name = object
|
||||
.get("name")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("开局归处")
|
||||
.to_string();
|
||||
let description = object
|
||||
.get("description")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("玩家进入世界后的第一处落脚点。")
|
||||
.to_string();
|
||||
object.insert("id".to_string(), JsonValue::String("camp-1".to_string()));
|
||||
object.insert("kind".to_string(), JsonValue::String("camp".to_string()));
|
||||
object.insert("name".to_string(), JsonValue::String(name));
|
||||
object.insert("description".to_string(), JsonValue::String(description));
|
||||
JsonValue::Object(object)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SceneRoleRef {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn build_scene_chapter_blueprints_from_camp_and_landmarks(
|
||||
camp: &JsonValue,
|
||||
landmarks: &[JsonValue],
|
||||
scene_role_refs: &[SceneRoleRef],
|
||||
) -> Vec<JsonValue> {
|
||||
let mut blueprints = Vec::with_capacity(landmarks.len() + 1);
|
||||
blueprints.push(build_scene_chapter_blueprint_from_scene(
|
||||
@@ -1105,12 +1158,19 @@ fn build_scene_chapter_blueprints_from_camp_and_landmarks(
|
||||
0,
|
||||
"camp",
|
||||
"开局归处",
|
||||
scene_role_refs,
|
||||
));
|
||||
blueprints.extend(build_scene_chapter_blueprints_from_landmarks(
|
||||
landmarks,
|
||||
scene_role_refs,
|
||||
));
|
||||
blueprints.extend(build_scene_chapter_blueprints_from_landmarks(landmarks));
|
||||
blueprints
|
||||
}
|
||||
|
||||
fn build_scene_chapter_blueprints_from_landmarks(landmarks: &[JsonValue]) -> Vec<JsonValue> {
|
||||
fn build_scene_chapter_blueprints_from_landmarks(
|
||||
landmarks: &[JsonValue],
|
||||
scene_role_refs: &[SceneRoleRef],
|
||||
) -> Vec<JsonValue> {
|
||||
// 幕背景描述必须来自关键场景生成步骤,不能在草稿合成阶段再用规则句拼接。
|
||||
landmarks
|
||||
.iter()
|
||||
@@ -1121,6 +1181,7 @@ fn build_scene_chapter_blueprints_from_landmarks(landmarks: &[JsonValue]) -> Vec
|
||||
chapter_index,
|
||||
"saved-landmark",
|
||||
"关键场景",
|
||||
scene_role_refs,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
@@ -1131,6 +1192,7 @@ fn build_scene_chapter_blueprint_from_scene(
|
||||
chapter_index: usize,
|
||||
id_prefix: &str,
|
||||
fallback_name_prefix: &str,
|
||||
scene_role_refs: &[SceneRoleRef],
|
||||
) -> JsonValue {
|
||||
let scene_name = json_text(scene, "name")
|
||||
.unwrap_or_else(|| format!("{}{}", fallback_name_prefix, chapter_index + 1));
|
||||
@@ -1144,6 +1206,13 @@ fn build_scene_chapter_blueprint_from_scene(
|
||||
let act_npc_names = json_string_array(scene, "actNPCNames")
|
||||
.or_else(|| json_string_array(scene, "sceneNpcNames"))
|
||||
.unwrap_or_default();
|
||||
let resolved_act_roles = resolve_scene_act_roles(&act_npc_names, scene_role_refs);
|
||||
let scene_npc_ids = dedupe_text_values(
|
||||
&resolved_act_roles
|
||||
.iter()
|
||||
.map(|role| role.id.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
json!({
|
||||
"id": scene_id.clone(),
|
||||
@@ -1152,13 +1221,14 @@ fn build_scene_chapter_blueprint_from_scene(
|
||||
"summary": summary,
|
||||
"sceneTaskDescription": scene_task_description,
|
||||
"linkedLandmarkIds": [scene_id.clone()],
|
||||
"sceneNpcIds": scene_npc_ids,
|
||||
"acts": (0..3)
|
||||
.map(|act_index| build_scene_act_blueprint_from_landmark(
|
||||
&scene_id,
|
||||
&summary,
|
||||
&act_prompts,
|
||||
&act_events,
|
||||
&act_npc_names,
|
||||
&resolved_act_roles,
|
||||
act_index,
|
||||
))
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -1170,7 +1240,7 @@ fn build_scene_act_blueprint_from_landmark(
|
||||
scene_summary: &str,
|
||||
act_prompts: &[String],
|
||||
act_events: &[String],
|
||||
act_npc_names: &[String],
|
||||
act_roles: &[SceneRoleRef],
|
||||
act_index: usize,
|
||||
) -> JsonValue {
|
||||
let act_title = if act_index == 0 {
|
||||
@@ -1184,10 +1254,17 @@ fn build_scene_act_blueprint_from_landmark(
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
let opposite_npc_id = act_npc_names
|
||||
let opposite_role = act_roles
|
||||
.get(act_index)
|
||||
.or_else(|| act_npc_names.first())
|
||||
.cloned()
|
||||
.or_else(|| act_roles.first())
|
||||
.cloned();
|
||||
let opposite_npc_id = opposite_role
|
||||
.as_ref()
|
||||
.map(|role| role.id.clone())
|
||||
.unwrap_or_default();
|
||||
let opposite_role_name = opposite_role
|
||||
.as_ref()
|
||||
.map(|role| role.name.clone())
|
||||
.unwrap_or_default();
|
||||
let event_description = act_events
|
||||
.get(act_index)
|
||||
@@ -1196,12 +1273,16 @@ fn build_scene_act_blueprint_from_landmark(
|
||||
.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)
|
||||
build_default_act_event_description(
|
||||
scene_summary,
|
||||
opposite_role_name.as_str(),
|
||||
act_index,
|
||||
)
|
||||
});
|
||||
let background_prompt = prompt.unwrap_or_else(|| {
|
||||
build_default_act_background_prompt(
|
||||
scene_summary,
|
||||
opposite_npc_id.as_str(),
|
||||
opposite_role_name.as_str(),
|
||||
event_description.as_str(),
|
||||
act_index,
|
||||
)
|
||||
@@ -1212,21 +1293,23 @@ fn build_scene_act_blueprint_from_landmark(
|
||||
"title": act_title,
|
||||
"summary": scene_summary,
|
||||
"backgroundPromptText": background_prompt,
|
||||
"encounterNpcIds": build_act_encounter_npc_ids(act_npc_names, opposite_npc_id.as_str()),
|
||||
"encounterNpcIds": build_act_encounter_npc_ids(act_roles, opposite_npc_id.as_str()),
|
||||
"primaryNpcId": opposite_npc_id,
|
||||
"oppositeNpcId": opposite_npc_id,
|
||||
"primaryRoleName": opposite_role_name,
|
||||
"oppositeRoleName": opposite_role_name,
|
||||
"eventDescription": event_description,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_act_encounter_npc_ids(act_npc_names: &[String], primary_npc_id: &str) -> Vec<String> {
|
||||
let mut names = Vec::with_capacity(act_npc_names.len().max(1));
|
||||
fn build_act_encounter_npc_ids(act_roles: &[SceneRoleRef], primary_npc_id: &str) -> Vec<String> {
|
||||
let mut names = Vec::with_capacity(act_roles.len().max(1));
|
||||
let primary = primary_npc_id.trim();
|
||||
if !primary.is_empty() {
|
||||
names.push(primary.to_string());
|
||||
}
|
||||
for name in act_npc_names {
|
||||
let normalized = name.trim();
|
||||
for role in act_roles {
|
||||
let normalized = role.id.trim();
|
||||
if normalized.is_empty() || names.iter().any(|item| item == normalized) {
|
||||
continue;
|
||||
}
|
||||
@@ -1235,6 +1318,98 @@ fn build_act_encounter_npc_ids(act_npc_names: &[String], primary_npc_id: &str) -
|
||||
names
|
||||
}
|
||||
|
||||
fn assign_role_ids(entries: Vec<JsonValue>, id_prefix: &str) -> Vec<JsonValue> {
|
||||
entries
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, entry)| assign_role_id(entry, id_prefix, index))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn assign_role_id(mut entry: JsonValue, id_prefix: &str, index: usize) -> JsonValue {
|
||||
let name = json_text(&entry, "name").unwrap_or_else(|| format!("角色{}", index + 1));
|
||||
let fallback_id = format!("{}-{}", id_prefix, stable_ascii_slug(name.as_str()));
|
||||
let Some(object) = entry.as_object_mut() else {
|
||||
return json!({
|
||||
"id": fallback_id,
|
||||
"name": name,
|
||||
});
|
||||
};
|
||||
if object
|
||||
.get("id")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(str::trim)
|
||||
.is_none_or(str::is_empty)
|
||||
{
|
||||
object.insert("id".to_string(), JsonValue::String(fallback_id));
|
||||
}
|
||||
entry
|
||||
}
|
||||
|
||||
fn collect_scene_role_refs(entries: &[JsonValue]) -> Vec<SceneRoleRef> {
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let name = json_text(entry, "name")?;
|
||||
let id = json_text(entry, "id")?;
|
||||
Some(SceneRoleRef { id, name })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_scene_act_roles(
|
||||
requested_role_names: &[String],
|
||||
scene_role_refs: &[SceneRoleRef],
|
||||
) -> Vec<SceneRoleRef> {
|
||||
let mut resolved = requested_role_names
|
||||
.iter()
|
||||
.filter_map(|name| resolve_scene_role_ref(name, scene_role_refs))
|
||||
.collect::<Vec<_>>();
|
||||
if resolved.is_empty() {
|
||||
resolved.extend(scene_role_refs.iter().take(3).cloned());
|
||||
}
|
||||
dedupe_scene_role_refs(resolved)
|
||||
}
|
||||
|
||||
fn resolve_scene_role_ref(
|
||||
name_or_id: &str,
|
||||
scene_role_refs: &[SceneRoleRef],
|
||||
) -> Option<SceneRoleRef> {
|
||||
let normalized = name_or_id.trim();
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
scene_role_refs
|
||||
.iter()
|
||||
.find(|role| role.name == normalized || role.id == normalized)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn dedupe_scene_role_refs(entries: Vec<SceneRoleRef>) -> Vec<SceneRoleRef> {
|
||||
let mut seen = Vec::new();
|
||||
let mut result = Vec::new();
|
||||
for entry in entries {
|
||||
if entry.id.trim().is_empty() || seen.iter().any(|id| id == &entry.id) {
|
||||
continue;
|
||||
}
|
||||
seen.push(entry.id.clone());
|
||||
result.push(entry);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn dedupe_text_values(values: &[String]) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
for value in values {
|
||||
let normalized = value.trim();
|
||||
if normalized.is_empty() || result.iter().any(|item| item == normalized) {
|
||||
continue;
|
||||
}
|
||||
result.push(normalized.to_string());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn build_default_scene_task_description(scene_name: &str, scene_summary: &str) -> String {
|
||||
if scene_summary.trim().is_empty() {
|
||||
return format!(
|
||||
@@ -1374,66 +1549,15 @@ fn normalize_framework_shape(framework: &mut JsonValue, setting_text: &str) {
|
||||
"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| {
|
||||
let event_description = build_default_act_event_description(
|
||||
camp_description.as_str(),
|
||||
"开局关键角色",
|
||||
index,
|
||||
);
|
||||
JsonValue::String(build_default_act_background_prompt(
|
||||
camp_description.as_str(),
|
||||
"开局关键角色",
|
||||
event_description.as_str(),
|
||||
index,
|
||||
))
|
||||
})
|
||||
.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(),
|
||||
),
|
||||
);
|
||||
// 中文注释:framework 只保留开局归处占位;完整开局场景任务与三幕内容统一交给场景批生成阶段。
|
||||
for generated_scene_key in [
|
||||
"sceneTaskDescription",
|
||||
"actBackgroundPromptTexts",
|
||||
"actEventDescriptions",
|
||||
"actNPCNames",
|
||||
"sceneNpcNames",
|
||||
] {
|
||||
camp.remove(generated_scene_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2024,7 +2148,18 @@ mod tests {
|
||||
"actNPCNames": ["灯童丁", "档吏庚", "灯童丁"]
|
||||
})];
|
||||
|
||||
let blueprints = build_scene_chapter_blueprints_from_landmarks(&landmarks);
|
||||
let scene_role_refs = vec![
|
||||
SceneRoleRef {
|
||||
id: "story-npc-lamp-child".to_string(),
|
||||
name: "灯童丁".to_string(),
|
||||
},
|
||||
SceneRoleRef {
|
||||
id: "story-npc-archive-clerk".to_string(),
|
||||
name: "档吏庚".to_string(),
|
||||
},
|
||||
];
|
||||
let blueprints =
|
||||
build_scene_chapter_blueprints_from_landmarks(&landmarks, &scene_role_refs);
|
||||
let acts = blueprints[0]
|
||||
.get("acts")
|
||||
.and_then(JsonValue::as_array)
|
||||
@@ -2043,10 +2178,23 @@ mod tests {
|
||||
"首次进入雾港码头时,查明黑潮船骨与灯童丁目击证词的关联。"
|
||||
))
|
||||
);
|
||||
assert_eq!(acts[0].get("oppositeNpcId"), Some(&json!("灯童丁")));
|
||||
assert_eq!(acts[0].get("primaryNpcId"), Some(&json!("灯童丁")));
|
||||
assert_eq!(acts[1].get("oppositeNpcId"), Some(&json!("档吏庚")));
|
||||
assert_eq!(acts[1].get("primaryNpcId"), Some(&json!("档吏庚")));
|
||||
assert_eq!(
|
||||
acts[0].get("oppositeNpcId"),
|
||||
Some(&json!("story-npc-lamp-child"))
|
||||
);
|
||||
assert_eq!(
|
||||
acts[0].get("primaryNpcId"),
|
||||
Some(&json!("story-npc-lamp-child"))
|
||||
);
|
||||
assert_eq!(acts[0].get("primaryRoleName"), Some(&json!("灯童丁")));
|
||||
assert_eq!(
|
||||
acts[1].get("oppositeNpcId"),
|
||||
Some(&json!("story-npc-archive-clerk"))
|
||||
);
|
||||
assert_eq!(
|
||||
acts[1].get("primaryNpcId"),
|
||||
Some(&json!("story-npc-archive-clerk"))
|
||||
);
|
||||
assert_eq!(
|
||||
acts[0].get("eventDescription"),
|
||||
Some(&json!(
|
||||
@@ -2081,7 +2229,16 @@ mod tests {
|
||||
"actBackgroundPromptTexts": ["斗技台晨雾未散,石阶旁少年们列队观望。", "木桩与兵器架围出训练区,族徽旗帜在风里猎猎。", "暮色压下斗技场,中央擂台留出一对一交锋空间。"]
|
||||
})];
|
||||
|
||||
let blueprints = build_scene_chapter_blueprints_from_camp_and_landmarks(camp, &landmarks);
|
||||
let scene_role_refs = vec![SceneRoleRef {
|
||||
id: "story-npc-mentor".to_string(),
|
||||
name: "药师长老".to_string(),
|
||||
}];
|
||||
|
||||
let blueprints = build_scene_chapter_blueprints_from_camp_and_landmarks(
|
||||
camp,
|
||||
&landmarks,
|
||||
&scene_role_refs,
|
||||
);
|
||||
let opening_chapter = &blueprints[0];
|
||||
let opening_acts = opening_chapter
|
||||
.get("acts")
|
||||
@@ -2106,6 +2263,18 @@ mod tests {
|
||||
.and_then(JsonValue::as_str)
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
}));
|
||||
assert!(opening_acts.iter().all(|act| {
|
||||
act.get("primaryNpcId")
|
||||
.and_then(JsonValue::as_str)
|
||||
.is_some_and(|value| value == "story-npc-mentor")
|
||||
}));
|
||||
assert!(opening_acts.iter().all(|act| {
|
||||
act.get("encounterNpcIds")
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|items| items.first())
|
||||
.and_then(JsonValue::as_str)
|
||||
.is_some_and(|value| value == "story-npc-mentor")
|
||||
}));
|
||||
assert_eq!(blueprints.len(), 2);
|
||||
}
|
||||
|
||||
@@ -2377,7 +2546,11 @@ mod tests {
|
||||
assert!(request_text.contains("attributeSchema"));
|
||||
assert!(request_text.contains("可扮演角色框架名单"));
|
||||
assert!(request_text.contains("场景角色框架名单"));
|
||||
assert!(request_text.contains("关键场景框架名单"));
|
||||
assert!(request_text.contains("场景框架名单"));
|
||||
assert!(request_text.contains("第一条场景必须是玩家进入世界时所在的开局场景"));
|
||||
assert!(request_text.contains("camp 只表示玩家开局时的落脚处占位"));
|
||||
assert!(!request_text.contains("camp.sceneTaskDescription"));
|
||||
assert!(!request_text.contains("camp.actBackgroundPromptTexts"));
|
||||
assert!(request_text.contains("actNPCNames"));
|
||||
assert!(!request_text.contains("\"sceneNpcNames\""));
|
||||
assert!(request_text.contains("connectedLandmarkNames"));
|
||||
@@ -2434,6 +2607,43 @@ mod tests {
|
||||
.and_then(JsonValue::as_str)
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("camp")
|
||||
.and_then(|entry| entry.get("name"))
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("旧灯塔")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("camp")
|
||||
.and_then(|entry| entry.get("id"))
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("camp-1")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("camp")
|
||||
.and_then(|entry| entry.get("sceneTaskDescription"))
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("首次进入旧灯塔时,追查被篡改的灯火航线记录。")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("landmarks")
|
||||
.and_then(JsonValue::as_array)
|
||||
.map(Vec::len),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("landmarks")
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|entries| entries.first())
|
||||
.and_then(|entry| entry.get("name"))
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("沉船湾")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("sceneChapterBlueprints")
|
||||
@@ -2462,19 +2672,57 @@ mod tests {
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|items| items.first())
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("灯童丁")
|
||||
Some("船魂戊")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("sceneChapterBlueprints")
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|entries| entries.get(1))
|
||||
.and_then(|entries| entries.first())
|
||||
.and_then(|entry| entry.get("acts"))
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|acts| acts.get(1))
|
||||
.and_then(|act| act.get("primaryNpcId"))
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("档吏庚")
|
||||
Some("story-npc-0192680e")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("sceneChapterBlueprints")
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|entries| entries.first())
|
||||
.and_then(|entry| entry.get("acts"))
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|acts| acts.first())
|
||||
.and_then(|act| act.get("primaryNpcId"))
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("story-npc-01b5406b")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("sceneChapterBlueprints")
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|entries| entries.first())
|
||||
.and_then(|entry| entry.get("acts"))
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|acts| acts.first())
|
||||
.and_then(|act| act.get("encounterNpcIds"))
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|items| items.first())
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("story-npc-01b5406b")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("sceneChapterBlueprints")
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|entries| entries.first())
|
||||
.and_then(|entry| entry.get("acts"))
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|acts| acts.first())
|
||||
.and_then(|act| act.get("primaryRoleName"))
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("灯童丁")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
@@ -2486,8 +2734,54 @@ mod tests {
|
||||
.and_then(|acts| acts.first())
|
||||
.and_then(|act| act.get("primaryNpcId"))
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("灯童丁")
|
||||
Some("story-npc-01fc0701")
|
||||
);
|
||||
assert_eq!(
|
||||
draft_profile
|
||||
.get("sceneChapterBlueprints")
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|entries| entries.get(1))
|
||||
.and_then(|entry| entry.get("acts"))
|
||||
.and_then(JsonValue::as_array)
|
||||
.and_then(|acts| acts.get(1))
|
||||
.and_then(|act| act.get("primaryNpcId"))
|
||||
.and_then(JsonValue::as_str),
|
||||
Some("story-npc-01acae6c")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_scene_batch_first_entry_becomes_opening_camp() {
|
||||
let fallback_camp = json!({
|
||||
"name": "世界骨架占位归处",
|
||||
"description": "只来自 framework 的轻量占位。"
|
||||
});
|
||||
let generated_scenes = vec![
|
||||
json!({
|
||||
"name": "旧灯塔",
|
||||
"description": "雾中仍亮着错位灯火",
|
||||
"sceneTaskDescription": "首次进入旧灯塔时,追查被篡改的灯火航线记录。",
|
||||
"actBackgroundPromptTexts": ["一", "二", "三"],
|
||||
"actEventDescriptions": ["甲", "乙", "丙"],
|
||||
}),
|
||||
json!({
|
||||
"name": "沉船湾",
|
||||
"description": "退潮后露出旧船骨"
|
||||
}),
|
||||
];
|
||||
|
||||
let (camp, landmarks) =
|
||||
split_generated_scenes_into_camp_and_landmarks(fallback_camp, generated_scenes);
|
||||
|
||||
assert_eq!(camp.get("id"), Some(&json!("camp-1")));
|
||||
assert_eq!(camp.get("kind"), Some(&json!("camp")));
|
||||
assert_eq!(camp.get("name"), Some(&json!("旧灯塔")));
|
||||
assert_eq!(
|
||||
camp.get("sceneTaskDescription"),
|
||||
Some(&json!("首次进入旧灯塔时,追查被篡改的灯火航线记录。"))
|
||||
);
|
||||
assert_eq!(landmarks.len(), 1);
|
||||
assert_eq!(landmarks[0].get("name"), Some(&json!("沉船湾")));
|
||||
}
|
||||
|
||||
fn llm_response(content: &str) -> String {
|
||||
|
||||
Reference in New Issue
Block a user