fix: preserve rpg custom world detail profiles
This commit is contained in:
@@ -5,8 +5,8 @@ use shared_contracts::runtime_story::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
battle::resolve_battle_action, build_status_patch, read_bool_field, read_i32_field,
|
||||
read_optional_string_field,
|
||||
StoryRuntimeActionResolveInput, battle::resolve_battle_action, build_status_patch,
|
||||
read_bool_field, read_i32_field, read_optional_string_field, resolve_story_runtime_action,
|
||||
};
|
||||
|
||||
fn build_battle_fixture() -> serde_json::Value {
|
||||
@@ -61,6 +61,115 @@ fn build_request(function_id: &str, option_text: &str) -> RuntimeStoryActionRequ
|
||||
}
|
||||
}
|
||||
|
||||
fn build_runtime_action_request(
|
||||
function_id: &str,
|
||||
action_text: &str,
|
||||
payload: Option<serde_json::Value>,
|
||||
) -> shared_contracts::story::ResolveStoryRuntimeActionRequest {
|
||||
shared_contracts::story::ResolveStoryRuntimeActionRequest {
|
||||
story_session_id: "storysess-1".to_string(),
|
||||
client_version: Some(1),
|
||||
function_id: function_id.to_string(),
|
||||
action_text: action_text.to_string(),
|
||||
target_id: None,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_custom_world_profile_with_two_landmarks() -> serde_json::Value {
|
||||
json!({
|
||||
"id": "profile-1",
|
||||
"name": "雾桥旧约",
|
||||
"summary": "雾桥边的旧约正在复苏。",
|
||||
"camp": {
|
||||
"id": "camp-1",
|
||||
"name": "雾桥营地",
|
||||
"description": "营火压着雾气。",
|
||||
"connections": [
|
||||
{
|
||||
"targetLandmarkId": "landmark-1",
|
||||
"relativePosition": "forward",
|
||||
"summary": "沿桥面继续前进"
|
||||
},
|
||||
{
|
||||
"targetLandmarkId": "landmark-2",
|
||||
"relativePosition": "right",
|
||||
"summary": "转入雾中支路"
|
||||
}
|
||||
]
|
||||
},
|
||||
"landmarks": [
|
||||
{
|
||||
"id": "landmark-1",
|
||||
"name": "断桥口",
|
||||
"description": "桥口挂着旧灯。"
|
||||
},
|
||||
{
|
||||
"id": "landmark-2",
|
||||
"name": "雾中渡",
|
||||
"description": "渡口只有潮声。"
|
||||
}
|
||||
],
|
||||
"storyNpcs": [
|
||||
{
|
||||
"id": "npc-bridge",
|
||||
"name": "桥影",
|
||||
"description": "桥下逼来的敌影",
|
||||
"initialAffinity": -20
|
||||
},
|
||||
{
|
||||
"id": "npc-ferryman",
|
||||
"name": "摆渡人",
|
||||
"description": "守着雾中渡的人",
|
||||
"initialAffinity": 0
|
||||
}
|
||||
],
|
||||
"sceneChapterBlueprints": [
|
||||
{
|
||||
"id": "chapter-camp",
|
||||
"sceneId": "camp-1",
|
||||
"linkedLandmarkIds": ["camp-1"],
|
||||
"acts": [
|
||||
{
|
||||
"id": "act-camp-1",
|
||||
"sceneId": "camp-1",
|
||||
"oppositeNpcId": "npc-bridge"
|
||||
},
|
||||
{
|
||||
"id": "act-camp-2",
|
||||
"sceneId": "camp-1",
|
||||
"oppositeNpcId": "npc-ferryman"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chapter-landmark-1",
|
||||
"sceneId": "landmark-1",
|
||||
"linkedLandmarkIds": ["landmark-1"],
|
||||
"acts": [
|
||||
{
|
||||
"id": "act-landmark-1",
|
||||
"sceneId": "landmark-1",
|
||||
"oppositeNpcId": "npc-ferryman"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
fn build_story_runtime_snapshot(
|
||||
game_state: serde_json::Value,
|
||||
current_story: Option<serde_json::Value>,
|
||||
) -> shared_contracts::story::StoryRuntimeSnapshotPayload {
|
||||
shared_contracts::story::StoryRuntimeSnapshotPayload {
|
||||
saved_at: None,
|
||||
bottom_tab: "adventure".to_string(),
|
||||
game_state,
|
||||
current_story,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battle_resolution_prefers_player_defeat_when_both_sides_fall_in_same_turn() {
|
||||
let request = build_request("battle_all_in_crush", "全力压制");
|
||||
@@ -89,3 +198,210 @@ fn battle_resolution_prefers_player_defeat_when_both_sides_fall_in_same_turn() {
|
||||
Some("defeat".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_battle_action_persists_post_battle_continue_story() {
|
||||
let mut game_state = build_battle_fixture();
|
||||
game_state["runtimeSessionId"] = json!("runtime-1");
|
||||
game_state["currentScene"] = json!("Story");
|
||||
game_state["worldType"] = json!("CUSTOM");
|
||||
game_state["playerHp"] = json!(30);
|
||||
game_state["customWorldProfile"] = build_custom_world_profile_with_two_landmarks();
|
||||
game_state["currentScenePreset"] = json!({
|
||||
"id": "custom-scene-camp",
|
||||
"name": "雾桥营地",
|
||||
"description": "营火压着雾气。",
|
||||
"connectedSceneIds": ["custom-scene-landmark-1", "custom-scene-landmark-2"],
|
||||
"forwardSceneId": "custom-scene-landmark-1",
|
||||
"treasureHints": [],
|
||||
"npcs": []
|
||||
});
|
||||
game_state["storyEngineMemory"] = json!({
|
||||
"currentSceneActState": {
|
||||
"sceneId": "camp-1",
|
||||
"chapterId": "chapter-camp",
|
||||
"currentActId": "act-camp-1",
|
||||
"currentActIndex": 0,
|
||||
"completedActIds": [],
|
||||
"visitedActIds": ["act-camp-1"]
|
||||
}
|
||||
});
|
||||
|
||||
let output = resolve_story_runtime_action(StoryRuntimeActionResolveInput {
|
||||
story_session_id: "storysess-1".to_string(),
|
||||
runtime_session_id: "runtime-1".to_string(),
|
||||
snapshot: build_story_runtime_snapshot(game_state, None),
|
||||
request: build_runtime_action_request("battle_all_in_crush", "全力压制", None),
|
||||
})
|
||||
.expect("terminal battle should resolve");
|
||||
|
||||
assert_eq!(
|
||||
output.presentation.battle.unwrap().outcome.as_deref(),
|
||||
Some("victory")
|
||||
);
|
||||
assert_eq!(
|
||||
output.presentation.options[0].function_id,
|
||||
"story_continue_adventure"
|
||||
);
|
||||
assert_eq!(
|
||||
output.snapshot.current_story.as_ref().unwrap()["options"][0]["functionId"],
|
||||
json!("story_continue_adventure")
|
||||
);
|
||||
assert!(
|
||||
output.snapshot.current_story.as_ref().unwrap()["deferredOptions"]
|
||||
.as_array()
|
||||
.is_some_and(|items| {
|
||||
items
|
||||
.iter()
|
||||
.any(|item| item["functionId"] == json!("idle_travel_next_scene"))
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
output.snapshot.current_story.as_ref().unwrap()["deferredRuntimeState"]["storyEngineMemory"]
|
||||
["currentSceneActState"]["currentActId"],
|
||||
json!("act-camp-2")
|
||||
);
|
||||
assert_eq!(
|
||||
output.snapshot.game_state["storyEngineMemory"]["currentSceneActState"]["currentActId"],
|
||||
json!("act-camp-2")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_travel_next_scene_changes_scene_from_target_payload() {
|
||||
let game_state = json!({
|
||||
"runtimeSessionId": "runtime-1",
|
||||
"runtimeActionVersion": 1,
|
||||
"currentScene": "Story",
|
||||
"worldType": "CUSTOM",
|
||||
"customWorldProfile": build_custom_world_profile_with_two_landmarks(),
|
||||
"playerHp": 30,
|
||||
"playerMaxHp": 40,
|
||||
"playerMana": 10,
|
||||
"playerMaxMana": 20,
|
||||
"playerCurrency": 0,
|
||||
"playerInventory": [],
|
||||
"playerEquipment": { "weapon": null, "armor": null, "relic": null },
|
||||
"runtimeStats": {
|
||||
"hostileNpcsDefeated": 0,
|
||||
"itemsUsed": 0,
|
||||
"questsAccepted": 0,
|
||||
"scenesTraveled": 0,
|
||||
"playTimeMs": 0,
|
||||
"lastPlayTickAt": null
|
||||
},
|
||||
"currentScenePreset": {
|
||||
"id": "custom-scene-camp",
|
||||
"name": "雾桥营地",
|
||||
"description": "营火压着雾气。",
|
||||
"connectedSceneIds": ["custom-scene-landmark-1", "custom-scene-landmark-2"],
|
||||
"connections": [
|
||||
{
|
||||
"sceneId": "custom-scene-landmark-1",
|
||||
"relativePosition": "forward",
|
||||
"summary": "沿桥面继续前进"
|
||||
}
|
||||
],
|
||||
"forwardSceneId": "custom-scene-landmark-1",
|
||||
"treasureHints": [],
|
||||
"npcs": []
|
||||
},
|
||||
"currentEncounter": null,
|
||||
"npcInteractionActive": false,
|
||||
"sceneHostileNpcs": [],
|
||||
"inBattle": false,
|
||||
"storyHistory": [],
|
||||
"storyEngineMemory": {}
|
||||
});
|
||||
|
||||
let output = resolve_story_runtime_action(StoryRuntimeActionResolveInput {
|
||||
story_session_id: "storysess-1".to_string(),
|
||||
runtime_session_id: "runtime-1".to_string(),
|
||||
snapshot: build_story_runtime_snapshot(game_state, None),
|
||||
request: build_runtime_action_request(
|
||||
"idle_travel_next_scene",
|
||||
"向前走,前往断桥口",
|
||||
Some(json!({ "targetSceneId": "custom-scene-landmark-1" })),
|
||||
),
|
||||
})
|
||||
.expect("travel action should resolve");
|
||||
|
||||
assert_eq!(
|
||||
output.snapshot.game_state["currentScenePreset"]["id"],
|
||||
json!("custom-scene-landmark-1")
|
||||
);
|
||||
assert_eq!(
|
||||
output.snapshot.game_state["runtimeStats"]["scenesTraveled"],
|
||||
json!(1)
|
||||
);
|
||||
assert_eq!(
|
||||
output.snapshot.game_state["currentEncounter"]["id"],
|
||||
json!("npc-ferryman")
|
||||
);
|
||||
assert_eq!(
|
||||
output.snapshot.game_state["storyEngineMemory"]["currentSceneActState"]["currentActId"],
|
||||
json!("act-landmark-1")
|
||||
);
|
||||
assert!(output.presentation.options.iter().any(|option| {
|
||||
option.function_id == "idle_travel_next_scene"
|
||||
|| option.function_id == "idle_explore_forward"
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_travel_next_scene_normalizes_custom_landmark_id_payload() {
|
||||
let game_state = json!({
|
||||
"runtimeSessionId": "runtime-1",
|
||||
"runtimeActionVersion": 1,
|
||||
"currentScene": "Story",
|
||||
"worldType": "CUSTOM",
|
||||
"customWorldProfile": build_custom_world_profile_with_two_landmarks(),
|
||||
"playerHp": 30,
|
||||
"playerMaxHp": 40,
|
||||
"playerMana": 10,
|
||||
"playerMaxMana": 20,
|
||||
"playerCurrency": 0,
|
||||
"playerInventory": [],
|
||||
"playerEquipment": { "weapon": null, "armor": null, "relic": null },
|
||||
"runtimeStats": {
|
||||
"hostileNpcsDefeated": 0,
|
||||
"itemsUsed": 0,
|
||||
"questsAccepted": 0,
|
||||
"scenesTraveled": 0,
|
||||
"playTimeMs": 0,
|
||||
"lastPlayTickAt": null
|
||||
},
|
||||
"currentScenePreset": {
|
||||
"id": "custom-scene-camp",
|
||||
"name": "雾桥营地",
|
||||
"description": "营火压着雾气。",
|
||||
"connectedSceneIds": ["landmark-1", "landmark-2"],
|
||||
"forwardSceneId": "landmark-2",
|
||||
"treasureHints": [],
|
||||
"npcs": []
|
||||
},
|
||||
"currentEncounter": null,
|
||||
"npcInteractionActive": false,
|
||||
"sceneHostileNpcs": [],
|
||||
"inBattle": false,
|
||||
"storyHistory": [],
|
||||
"storyEngineMemory": {}
|
||||
});
|
||||
|
||||
let output = resolve_story_runtime_action(StoryRuntimeActionResolveInput {
|
||||
story_session_id: "storysess-1".to_string(),
|
||||
runtime_session_id: "runtime-1".to_string(),
|
||||
snapshot: build_story_runtime_snapshot(game_state, None),
|
||||
request: build_runtime_action_request(
|
||||
"idle_travel_next_scene",
|
||||
"前往雾中渡",
|
||||
Some(json!({ "targetSceneId": "landmark-2" })),
|
||||
),
|
||||
})
|
||||
.expect("raw custom landmark id should resolve");
|
||||
|
||||
assert_eq!(
|
||||
output.snapshot.game_state["currentScenePreset"]["id"],
|
||||
json!("custom-scene-landmark-2")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user