fix: preserve rpg custom world detail profiles
This commit is contained in:
@@ -2,10 +2,11 @@ use serde_json::{Value, json};
|
||||
use shared_contracts::runtime_story::RuntimeStoryOptionView;
|
||||
|
||||
use crate::{
|
||||
CONTINUE_ADVENTURE_FUNCTION_ID, build_static_runtime_story_option,
|
||||
CONTINUE_ADVENTURE_FUNCTION_ID, build_custom_scene_preset, build_static_runtime_story_option,
|
||||
build_story_option_from_runtime_option, ensure_json_object, read_array_field, read_bool_field,
|
||||
read_field, read_i32_field, read_object_field, read_optional_string_field, write_bool_field,
|
||||
write_i32_field, write_null_field, write_string_field,
|
||||
read_field, read_i32_field, read_object_field, read_optional_string_field,
|
||||
resolve_custom_runtime_scene_id, write_bool_field, write_i32_field, write_null_field,
|
||||
write_string_field,
|
||||
};
|
||||
|
||||
const WUXIA_FIRST_SCENE_ID: &str = "wuxia-bamboo-road";
|
||||
@@ -36,6 +37,8 @@ pub fn finalize_post_battle_resolution(
|
||||
return None;
|
||||
}
|
||||
|
||||
let original_scene_act_state = current_scene_act_state(game_state);
|
||||
|
||||
if outcome == "defeat" {
|
||||
return Some(finalize_defeat_revive(game_state, fallback_options));
|
||||
}
|
||||
@@ -45,6 +48,7 @@ pub fn finalize_post_battle_resolution(
|
||||
game_state,
|
||||
result_text,
|
||||
fallback_options,
|
||||
original_scene_act_state,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -64,13 +68,14 @@ fn finalize_victory_or_spar(
|
||||
game_state: &mut Value,
|
||||
result_text: &str,
|
||||
fallback_options: Vec<RuntimeStoryOptionView>,
|
||||
original_scene_act_state: Option<Value>,
|
||||
) -> PostBattleFinalization {
|
||||
clear_post_battle_state(game_state);
|
||||
let is_last_act = is_current_scene_act_last(game_state);
|
||||
let next_act_state = if is_last_act {
|
||||
None
|
||||
} else {
|
||||
resolve_next_scene_act_runtime_state(game_state)
|
||||
resolve_next_scene_act_runtime_state(game_state, original_scene_act_state.as_ref())
|
||||
};
|
||||
if let Some(next_act_state) = next_act_state {
|
||||
write_current_scene_act_state(game_state, next_act_state);
|
||||
@@ -141,7 +146,7 @@ fn finalize_defeat_revive(
|
||||
{
|
||||
write_current_scene_act_state(game_state, first_act_state);
|
||||
}
|
||||
ensure_first_scene_encounter_preview(game_state);
|
||||
ensure_scene_encounter_preview(game_state);
|
||||
|
||||
let story_text = if first_scene.name.is_empty() {
|
||||
"你在战斗中倒下,随后重新醒来。".to_string()
|
||||
@@ -160,7 +165,7 @@ fn finalize_defeat_revive(
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_post_battle_state(game_state: &mut Value) {
|
||||
pub fn clear_post_battle_state(game_state: &mut Value) {
|
||||
write_null_field(game_state, "currentEncounter");
|
||||
write_bool_field(game_state, "npcInteractionActive", false);
|
||||
ensure_json_object(game_state).insert("sceneHostileNpcs".to_string(), Value::Array(Vec::new()));
|
||||
@@ -421,7 +426,7 @@ fn write_first_scene(game_state: &mut Value, scene: &RuntimeScene) {
|
||||
);
|
||||
}
|
||||
|
||||
fn ensure_first_scene_encounter_preview(game_state: &mut Value) {
|
||||
pub fn ensure_scene_encounter_preview(game_state: &mut Value) {
|
||||
if read_bool_field(game_state, "inBattle").unwrap_or(false) {
|
||||
return;
|
||||
}
|
||||
@@ -436,7 +441,13 @@ fn ensure_first_scene_encounter_preview(game_state: &mut Value) {
|
||||
};
|
||||
let scene_id = read_object_field(game_state, "currentScenePreset")
|
||||
.and_then(|scene| read_optional_string_field(scene, "id"));
|
||||
let focus_npc_id = resolve_active_scene_act_focus_npc_id(profile, scene_id.as_deref());
|
||||
let current_act_id = current_scene_act_state(game_state)
|
||||
.and_then(|state| read_optional_string_field(&state, "currentActId"));
|
||||
let focus_npc_id = resolve_active_scene_act_focus_npc_id(
|
||||
profile,
|
||||
scene_id.as_deref(),
|
||||
current_act_id.as_deref(),
|
||||
);
|
||||
let Some(focus_npc_id) = focus_npc_id else {
|
||||
return;
|
||||
};
|
||||
@@ -450,6 +461,22 @@ fn ensure_first_scene_encounter_preview(game_state: &mut Value) {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn ensure_scene_act_state(game_state: &mut Value) {
|
||||
if read_bool_field(game_state, "inBattle").unwrap_or(false) {
|
||||
return;
|
||||
}
|
||||
let Some(scene_id) = read_object_field(game_state, "currentScenePreset")
|
||||
.and_then(|scene| read_optional_string_field(scene, "id"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(act_state) = build_initial_scene_act_runtime_state(game_state, scene_id.as_str())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
write_current_scene_act_state(game_state, act_state);
|
||||
}
|
||||
|
||||
fn build_scene_travel_options(game_state: &Value) -> Vec<RuntimeStoryOptionView> {
|
||||
let Some(current_scene) = read_object_field(game_state, "currentScenePreset") else {
|
||||
return vec![build_static_runtime_story_option(
|
||||
@@ -459,32 +486,53 @@ fn build_scene_travel_options(game_state: &Value) -> Vec<RuntimeStoryOptionView>
|
||||
)];
|
||||
};
|
||||
let current_scene_id = read_optional_string_field(current_scene, "id");
|
||||
let mut options = read_array_field(current_scene, "connections")
|
||||
let forward_scene_id = read_optional_string_field(current_scene, "forwardSceneId");
|
||||
let mut option_scene_ids = Vec::new();
|
||||
let mut options = Vec::new();
|
||||
|
||||
for connection in read_array_field(current_scene, "connections") {
|
||||
let Some(scene_id) = read_optional_string_field(connection, "sceneId") else {
|
||||
continue;
|
||||
};
|
||||
if current_scene_id.as_deref() == Some(scene_id.as_str())
|
||||
|| option_scene_ids.iter().any(|id| id == scene_id.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative_position = read_optional_string_field(connection, "relativePosition")
|
||||
.unwrap_or_else(|| "forward".to_string());
|
||||
options.push(build_scene_travel_option(
|
||||
game_state,
|
||||
scene_id.as_str(),
|
||||
relative_position.as_str(),
|
||||
));
|
||||
option_scene_ids.push(scene_id);
|
||||
}
|
||||
|
||||
for scene_id in read_array_field(current_scene, "connectedSceneIds")
|
||||
.into_iter()
|
||||
.filter_map(|connection| {
|
||||
let scene_id = read_optional_string_field(connection, "sceneId")?;
|
||||
if current_scene_id.as_deref() == Some(scene_id.as_str()) {
|
||||
return None;
|
||||
}
|
||||
let relative_position = read_optional_string_field(connection, "relativePosition")
|
||||
.unwrap_or_else(|| "forward".to_string());
|
||||
let scene_name = resolve_scene_name(game_state, scene_id.as_str())
|
||||
.unwrap_or_else(|| scene_id.clone());
|
||||
Some(RuntimeStoryOptionView {
|
||||
payload: Some(json!({ "targetSceneId": scene_id })),
|
||||
..build_static_runtime_story_option(
|
||||
"idle_travel_next_scene",
|
||||
format!(
|
||||
"{},前往{}",
|
||||
direction_text(relative_position.as_str()),
|
||||
scene_name
|
||||
)
|
||||
.as_str(),
|
||||
"story",
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.filter_map(|scene_id| scene_id.as_str().map(str::to_string))
|
||||
.chain(forward_scene_id.clone())
|
||||
{
|
||||
// 中文注释:bootstrap 生成的旧快照常只有 connectedSceneIds / forwardSceneId,
|
||||
// 没有展开 connections;这里也要生成旅行 action,避免战后只剩默认 idle 选项循环。
|
||||
if current_scene_id.as_deref() == Some(scene_id.as_str())
|
||||
|| option_scene_ids.iter().any(|id| id == scene_id.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative_position = if forward_scene_id.as_deref() == Some(scene_id.as_str()) {
|
||||
"forward"
|
||||
} else {
|
||||
"portal"
|
||||
};
|
||||
options.push(build_scene_travel_option(
|
||||
game_state,
|
||||
scene_id.as_str(),
|
||||
relative_position,
|
||||
));
|
||||
option_scene_ids.push(scene_id);
|
||||
}
|
||||
|
||||
if options.is_empty() {
|
||||
options.push(build_static_runtime_story_option(
|
||||
@@ -497,6 +545,163 @@ fn build_scene_travel_options(game_state: &Value) -> Vec<RuntimeStoryOptionView>
|
||||
options
|
||||
}
|
||||
|
||||
fn build_scene_travel_option(
|
||||
game_state: &Value,
|
||||
scene_id: &str,
|
||||
relative_position: &str,
|
||||
) -> RuntimeStoryOptionView {
|
||||
let scene_name =
|
||||
resolve_scene_name(game_state, scene_id).unwrap_or_else(|| scene_id.to_string());
|
||||
RuntimeStoryOptionView {
|
||||
payload: Some(json!({ "targetSceneId": scene_id })),
|
||||
..build_static_runtime_story_option(
|
||||
"idle_travel_next_scene",
|
||||
format!("{},前往{}", direction_text(relative_position), scene_name).as_str(),
|
||||
"story",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_runtime_scene_preset(game_state: &Value, scene_id: &str) -> Option<Value> {
|
||||
let normalized_scene_id = scene_id.trim();
|
||||
if normalized_scene_id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(profile) = read_object_field(game_state, "customWorldProfile")
|
||||
&& let Some(scene) = build_custom_scene_preset(
|
||||
profile,
|
||||
resolve_custom_runtime_scene_id(profile, normalized_scene_id).as_str(),
|
||||
)
|
||||
{
|
||||
return Some(scene);
|
||||
}
|
||||
|
||||
resolve_builtin_runtime_scene_preset(game_state, normalized_scene_id)
|
||||
}
|
||||
|
||||
pub fn resolve_forward_scene_id(game_state: &Value) -> Option<String> {
|
||||
read_object_field(game_state, "currentScenePreset").and_then(|scene| {
|
||||
read_optional_string_field(scene, "forwardSceneId")
|
||||
.or_else(|| {
|
||||
read_array_field(scene, "connections")
|
||||
.into_iter()
|
||||
.find_map(|connection| read_optional_string_field(connection, "sceneId"))
|
||||
})
|
||||
.or_else(|| {
|
||||
read_array_field(scene, "connectedSceneIds")
|
||||
.into_iter()
|
||||
.find_map(|scene_id| scene_id.as_str().map(str::to_string))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_builtin_runtime_scene_preset(game_state: &Value, scene_id: &str) -> Option<Value> {
|
||||
let template = builtin_runtime_scene_template(scene_id)?;
|
||||
Some(json!({
|
||||
"id": template.id,
|
||||
"name": template.name,
|
||||
"description": template.description,
|
||||
"imageSrc": read_object_field(game_state, "currentScenePreset")
|
||||
.and_then(|scene| read_optional_string_field(scene, "imageSrc"))
|
||||
.unwrap_or_default(),
|
||||
"connectedSceneIds": template.connected_scene_ids,
|
||||
"connections": template.connections,
|
||||
"forwardSceneId": template.forward_scene_id,
|
||||
"treasureHints": template.treasure_hints,
|
||||
"npcs": [],
|
||||
}))
|
||||
}
|
||||
|
||||
fn builtin_runtime_scene_template(scene_id: &str) -> Option<RuntimeScene> {
|
||||
let is_xianxia = matches!(
|
||||
scene_id,
|
||||
"xianxia-cloud-gate"
|
||||
| "xianxia-floating-isle"
|
||||
| "xianxia-celestial-corridor"
|
||||
| "xianxia-star-vessel"
|
||||
);
|
||||
if is_xianxia {
|
||||
return Some(RuntimeScene {
|
||||
id: scene_id.to_string(),
|
||||
name: match scene_id {
|
||||
"xianxia-floating-isle" => "浮空灵岛",
|
||||
"xianxia-celestial-corridor" => "天门长廊",
|
||||
"xianxia-star-vessel" => "星槎泊台",
|
||||
_ => XIANXIA_FIRST_SCENE_NAME,
|
||||
}
|
||||
.to_string(),
|
||||
description: match scene_id {
|
||||
"xianxia-floating-isle" => "浮岛边缘灵雾翻涌,远处有阵纹一明一暗。",
|
||||
"xianxia-celestial-corridor" => "长廊悬在云海上方,符光沿石柱缓慢游走。",
|
||||
"xianxia-star-vessel" => "星槎泊在云海边缘,船身仍有星砂微光。",
|
||||
_ => XIANXIA_FIRST_SCENE_DESCRIPTION,
|
||||
}
|
||||
.to_string(),
|
||||
image_src: String::new(),
|
||||
connected_scene_ids: vec![
|
||||
"xianxia-cloud-gate".to_string(),
|
||||
"xianxia-floating-isle".to_string(),
|
||||
"xianxia-celestial-corridor".to_string(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|id| id != scene_id)
|
||||
.collect(),
|
||||
connections: vec![json!({
|
||||
"sceneId": if scene_id == "xianxia-cloud-gate" { "xianxia-celestial-corridor" } else { "xianxia-cloud-gate" },
|
||||
"relativePosition": if scene_id == "xianxia-cloud-gate" { "forward" } else { "back" },
|
||||
"summary": "沿主路继续移动"
|
||||
})],
|
||||
forward_scene_id: Some(if scene_id == "xianxia-cloud-gate" {
|
||||
"xianxia-celestial-corridor".to_string()
|
||||
} else {
|
||||
"xianxia-cloud-gate".to_string()
|
||||
}),
|
||||
treasure_hints: vec!["云阶边缘的灵光残痕".to_string()],
|
||||
npcs: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
Some(RuntimeScene {
|
||||
id: scene_id.to_string(),
|
||||
name: match scene_id {
|
||||
"wuxia-mountain-gate" => "山门石阶",
|
||||
"wuxia-mist-woods" => "迷雾竹林",
|
||||
"wuxia-ferry-bridge" => "渡口断桥",
|
||||
_ => WUXIA_FIRST_SCENE_NAME,
|
||||
}
|
||||
.to_string(),
|
||||
description: match scene_id {
|
||||
"wuxia-mountain-gate" => "山门石阶覆着苔痕,旧旗在风里压得很低。",
|
||||
"wuxia-mist-woods" => "迷雾在竹林间翻卷,脚下泥印很快又被雾水抹平。",
|
||||
"wuxia-ferry-bridge" => "渡口断桥横在冷水上,桥边灯笼只剩半截残光。",
|
||||
_ => WUXIA_FIRST_SCENE_DESCRIPTION,
|
||||
}
|
||||
.to_string(),
|
||||
image_src: String::new(),
|
||||
connected_scene_ids: vec![
|
||||
"wuxia-bamboo-road".to_string(),
|
||||
"wuxia-mountain-gate".to_string(),
|
||||
"wuxia-mist-woods".to_string(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|id| id != scene_id)
|
||||
.collect(),
|
||||
connections: vec![json!({
|
||||
"sceneId": if scene_id == "wuxia-bamboo-road" { "wuxia-mountain-gate" } else { "wuxia-bamboo-road" },
|
||||
"relativePosition": if scene_id == "wuxia-bamboo-road" { "forward" } else { "back" },
|
||||
"summary": "沿主路继续移动"
|
||||
})],
|
||||
forward_scene_id: Some(if scene_id == "wuxia-bamboo-road" {
|
||||
"wuxia-mountain-gate".to_string()
|
||||
} else {
|
||||
"wuxia-bamboo-road".to_string()
|
||||
}),
|
||||
treasure_hints: vec!["路边半埋的旧物".to_string()],
|
||||
npcs: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_scene_name(game_state: &Value, scene_id: &str) -> Option<String> {
|
||||
if read_object_field(game_state, "currentScenePreset")
|
||||
.and_then(|scene| read_optional_string_field(scene, "id"))
|
||||
@@ -553,7 +758,10 @@ fn direction_text(relative_position: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_next_scene_act_runtime_state(game_state: &Value) -> Option<Value> {
|
||||
fn resolve_next_scene_act_runtime_state(
|
||||
game_state: &Value,
|
||||
current_act_state_override: Option<&Value>,
|
||||
) -> Option<Value> {
|
||||
let profile = read_object_field(game_state, "customWorldProfile")?;
|
||||
let scene_id = read_object_field(game_state, "currentScenePreset")
|
||||
.and_then(|scene| read_optional_string_field(scene, "id"));
|
||||
@@ -563,7 +771,9 @@ fn resolve_next_scene_act_runtime_state(game_state: &Value) -> Option<Value> {
|
||||
if acts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let runtime_state = build_initial_scene_act_runtime_state(game_state, scene_id_text)?;
|
||||
let runtime_state = current_act_state_override
|
||||
.cloned()
|
||||
.or_else(|| build_initial_scene_act_runtime_state(game_state, scene_id_text))?;
|
||||
let current_act_id = read_optional_string_field(&runtime_state, "currentActId");
|
||||
let current_index = acts
|
||||
.iter()
|
||||
@@ -762,9 +972,17 @@ fn resolve_scene_aliases(profile: &Value, scene_id: &str) -> Vec<String> {
|
||||
fn resolve_active_scene_act_focus_npc_id(
|
||||
profile: &Value,
|
||||
scene_id: Option<&str>,
|
||||
current_act_id: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let chapter = resolve_scene_chapter_blueprint(profile, scene_id)?;
|
||||
let act_state = read_array_field(chapter, "acts").first().copied()?;
|
||||
let acts = read_array_field(chapter, "acts");
|
||||
let act_state = current_act_id
|
||||
.and_then(|act_id| {
|
||||
acts.iter()
|
||||
.copied()
|
||||
.find(|act| read_optional_string_field(act, "id").as_deref() == Some(act_id))
|
||||
})
|
||||
.or_else(|| acts.first().copied())?;
|
||||
read_optional_string_field(act_state, "oppositeNpcId")
|
||||
.or_else(|| read_optional_string_field(act_state, "primaryNpcId"))
|
||||
.or_else(|| {
|
||||
|
||||
Reference in New Issue
Block a user