377 lines
15 KiB
Rust
377 lines
15 KiB
Rust
mod application;
|
|
mod commands;
|
|
mod domain;
|
|
mod errors;
|
|
mod events;
|
|
|
|
pub use application::*;
|
|
pub use commands::*;
|
|
pub use domain::*;
|
|
pub use errors::*;
|
|
pub use events::*;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use module_quest::{
|
|
QuestNarrativeBindingSnapshot, QuestNarrativeOrigin, QuestNarrativeType,
|
|
QuestObjectiveKind, QuestObjectiveSnapshot, QuestRecordSnapshot, QuestRewardEquipmentSlot,
|
|
QuestRewardItem, QuestRewardItemRarity, QuestRewardSnapshot, QuestStatus,
|
|
};
|
|
|
|
#[test]
|
|
fn validate_story_session_input_accepts_minimal_contract() {
|
|
let result = validate_story_session_input(&StorySessionInput {
|
|
story_session_id: "storysess_001".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
actor_user_id: "user_001".to_string(),
|
|
world_profile_id: "profile_001".to_string(),
|
|
initial_prompt: "进入营地".to_string(),
|
|
opening_summary: Some("营地开场".to_string()),
|
|
created_at_micros: 1_713_680_000_000_000,
|
|
});
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn validate_story_session_input_rejects_missing_required_fields() {
|
|
let error = validate_story_session_input(&StorySessionInput {
|
|
story_session_id: String::new(),
|
|
runtime_session_id: String::new(),
|
|
actor_user_id: String::new(),
|
|
world_profile_id: String::new(),
|
|
initial_prompt: String::new(),
|
|
opening_summary: None,
|
|
created_at_micros: 1,
|
|
})
|
|
.expect_err("missing required story session fields should fail");
|
|
|
|
assert_eq!(error, StorySessionFieldError::MissingSessionId);
|
|
}
|
|
|
|
#[test]
|
|
fn build_story_session_snapshot_uses_active_status_and_initial_version() {
|
|
let snapshot = build_story_session_snapshot(StorySessionInput {
|
|
story_session_id: "storysess_001".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
actor_user_id: "user_001".to_string(),
|
|
world_profile_id: "profile_001".to_string(),
|
|
initial_prompt: "进入营地".to_string(),
|
|
opening_summary: Some(" ".to_string()),
|
|
created_at_micros: 12,
|
|
});
|
|
|
|
assert_eq!(snapshot.status, StorySessionStatus::Active);
|
|
assert_eq!(snapshot.version, INITIAL_STORY_SESSION_VERSION);
|
|
assert_eq!(snapshot.opening_summary, None);
|
|
}
|
|
|
|
#[test]
|
|
fn build_story_session_input_normalizes_optional_summary() {
|
|
let input = build_story_session_input(
|
|
" storysess_001 ".to_string(),
|
|
" runtime_001 ".to_string(),
|
|
" user_001 ".to_string(),
|
|
" profile_001 ".to_string(),
|
|
" 进入营地 ".to_string(),
|
|
Some(" ".to_string()),
|
|
12,
|
|
)
|
|
.expect("story session input should build");
|
|
|
|
assert_eq!(input.story_session_id, "storysess_001");
|
|
assert_eq!(input.runtime_session_id, "runtime_001");
|
|
assert_eq!(input.actor_user_id, "user_001");
|
|
assert_eq!(input.world_profile_id, "profile_001");
|
|
assert_eq!(input.initial_prompt, "进入营地");
|
|
assert_eq!(input.opening_summary, None);
|
|
}
|
|
|
|
#[test]
|
|
fn build_story_session_state_input_rejects_blank_session_id() {
|
|
let error = build_story_session_state_input(" ".to_string())
|
|
.expect_err("blank story session id should fail");
|
|
|
|
assert_eq!(error, StorySessionFieldError::MissingSessionId);
|
|
}
|
|
|
|
#[test]
|
|
fn build_story_session_state_record_maps_all_events() {
|
|
let record = build_story_session_state_record(
|
|
StorySessionSnapshot {
|
|
story_session_id: "storysess_001".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
actor_user_id: "user_001".to_string(),
|
|
world_profile_id: "profile_001".to_string(),
|
|
initial_prompt: "进入营地".to_string(),
|
|
opening_summary: Some("营地开场".to_string()),
|
|
latest_narrative_text: "你看见篝火边有人招手。".to_string(),
|
|
latest_choice_function_id: Some("talk_to_npc".to_string()),
|
|
status: StorySessionStatus::Active,
|
|
version: 2,
|
|
created_at_micros: 1_713_686_400_000_000,
|
|
updated_at_micros: 1_713_686_401_234_567,
|
|
},
|
|
vec![
|
|
StoryEventSnapshot {
|
|
event_id: "storyevt_001".to_string(),
|
|
story_session_id: "storysess_001".to_string(),
|
|
event_kind: StoryEventKind::SessionStarted,
|
|
narrative_text: "营地开场".to_string(),
|
|
choice_function_id: None,
|
|
created_at_micros: 1_713_686_400_000_000,
|
|
},
|
|
StoryEventSnapshot {
|
|
event_id: "storyevt_002".to_string(),
|
|
story_session_id: "storysess_001".to_string(),
|
|
event_kind: StoryEventKind::StoryContinued,
|
|
narrative_text: "你看见篝火边有人招手。".to_string(),
|
|
choice_function_id: Some("talk_to_npc".to_string()),
|
|
created_at_micros: 1_713_686_401_234_567,
|
|
},
|
|
],
|
|
);
|
|
|
|
assert_eq!(record.session.story_session_id, "storysess_001");
|
|
assert_eq!(record.events.len(), 2);
|
|
assert_eq!(record.events[0].event_kind, "session_started");
|
|
assert_eq!(record.events[1].event_kind, "story_continued");
|
|
}
|
|
|
|
#[test]
|
|
fn build_story_session_result_record_formats_status_and_timestamps() {
|
|
let record = build_story_session_result_record(
|
|
StorySessionSnapshot {
|
|
story_session_id: "storysess_001".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
actor_user_id: "user_001".to_string(),
|
|
world_profile_id: "profile_001".to_string(),
|
|
initial_prompt: "进入营地".to_string(),
|
|
opening_summary: Some("营地开场".to_string()),
|
|
latest_narrative_text: "你看到营地中央的篝火。".to_string(),
|
|
latest_choice_function_id: Some("inspect_campfire".to_string()),
|
|
status: StorySessionStatus::Active,
|
|
version: 2,
|
|
created_at_micros: 1_713_686_400_000_000,
|
|
updated_at_micros: 1_713_686_401_234_567,
|
|
},
|
|
StoryEventSnapshot {
|
|
event_id: "storyevt_001".to_string(),
|
|
story_session_id: "storysess_001".to_string(),
|
|
event_kind: StoryEventKind::StoryContinued,
|
|
narrative_text: "你看到营地中央的篝火。".to_string(),
|
|
choice_function_id: Some("inspect_campfire".to_string()),
|
|
created_at_micros: 1_713_686_401_234_567,
|
|
},
|
|
);
|
|
|
|
assert_eq!(record.session.status, "active");
|
|
assert_eq!(record.session.created_at, "1713686400.000000Z");
|
|
assert_eq!(record.session.updated_at, "1713686401.234567Z");
|
|
assert_eq!(record.event.event_kind, "story_continued");
|
|
assert_eq!(record.event.created_at, "1713686401.234567Z");
|
|
}
|
|
|
|
#[test]
|
|
fn apply_story_continue_updates_latest_narrative_and_emits_event() {
|
|
let current = build_story_session_snapshot(StorySessionInput {
|
|
story_session_id: "storysess_001".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
actor_user_id: "user_001".to_string(),
|
|
world_profile_id: "profile_001".to_string(),
|
|
initial_prompt: "进入营地".to_string(),
|
|
opening_summary: Some("营地开场".to_string()),
|
|
created_at_micros: 10,
|
|
});
|
|
|
|
let (next, event) = apply_story_continue(
|
|
current,
|
|
StoryContinueInput {
|
|
story_session_id: "storysess_001".to_string(),
|
|
event_id: "storyevt_001".to_string(),
|
|
narrative_text: "你看见篝火边有人招手。".to_string(),
|
|
choice_function_id: Some("talk_to_npc".to_string()),
|
|
updated_at_micros: 20,
|
|
},
|
|
)
|
|
.expect("continue story should succeed");
|
|
|
|
assert_eq!(next.latest_narrative_text, "你看见篝火边有人招手。");
|
|
assert_eq!(
|
|
next.latest_choice_function_id.as_deref(),
|
|
Some("talk_to_npc")
|
|
);
|
|
assert_eq!(next.version, INITIAL_STORY_SESSION_VERSION + 1);
|
|
assert_eq!(event.event_kind, StoryEventKind::StoryContinued);
|
|
}
|
|
|
|
#[test]
|
|
fn combat_victory_settlement_plan_grants_items_xp_and_chapter_ledger() {
|
|
let plan = build_combat_victory_settlement_plan(&module_combat::BattleStateSnapshot {
|
|
battle_state_id: "battle_001".to_string(),
|
|
story_session_id: "storysess_001".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
actor_user_id: "user_001".to_string(),
|
|
chapter_id: Some("chapter_01".to_string()),
|
|
target_npc_id: "npc_bandit".to_string(),
|
|
target_name: "山匪".to_string(),
|
|
battle_mode: module_combat::BattleMode::Fight,
|
|
status: module_combat::BattleStatus::Resolved,
|
|
player_hp: 20,
|
|
player_max_hp: 30,
|
|
player_mana: 6,
|
|
player_max_mana: 10,
|
|
target_hp: 0,
|
|
target_max_hp: 18,
|
|
experience_reward: 35,
|
|
reward_items: vec![module_runtime_item::RuntimeItemRewardItemSnapshot {
|
|
item_id: "iron_token".to_string(),
|
|
category: "material".to_string(),
|
|
item_name: "铁制信物".to_string(),
|
|
description: Some("山匪随身携带的旧信物。".to_string()),
|
|
quantity: 1,
|
|
rarity: module_runtime_item::RuntimeItemRewardItemRarity::Uncommon,
|
|
tags: vec!["quest".to_string()],
|
|
stackable: true,
|
|
stack_key: "iron_token".to_string(),
|
|
equipment_slot_id: None,
|
|
}],
|
|
turn_index: 2,
|
|
last_action_function_id: Some("battle_use_skill".to_string()),
|
|
last_action_text: Some("破阵一击".to_string()),
|
|
last_result_text: Some("战斗结束。".to_string()),
|
|
last_damage_dealt: 18,
|
|
last_damage_taken: 0,
|
|
last_outcome: module_combat::CombatOutcome::Victory,
|
|
version: 3,
|
|
created_at_micros: 100,
|
|
updated_at_micros: 200,
|
|
});
|
|
|
|
assert_eq!(plan.inventory_mutations.len(), 1);
|
|
assert_eq!(
|
|
plan.progression_grant.as_ref().map(|input| input.amount),
|
|
Some(35)
|
|
);
|
|
assert_eq!(
|
|
plan.chapter_ledger
|
|
.as_ref()
|
|
.map(|input| input.granted_hostile_xp),
|
|
Some(35)
|
|
);
|
|
|
|
let module_inventory::InventoryMutation::GrantItem(input) =
|
|
&plan.inventory_mutations[0].mutation
|
|
else {
|
|
panic!("combat victory should grant inventory item");
|
|
};
|
|
assert_eq!(
|
|
input.item.source_reference_id.as_deref(),
|
|
Some("battle_001")
|
|
);
|
|
assert_eq!(
|
|
input.item.source_kind,
|
|
module_inventory::InventoryItemSourceKind::CombatDrop
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quest_turn_in_settlement_plan_grants_reward_items_and_quest_xp() {
|
|
let plan = build_quest_turn_in_settlement_plan(&QuestRecordSnapshot {
|
|
quest_id: "quest_001".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
story_session_id: Some("storysess_001".to_string()),
|
|
actor_user_id: "user_001".to_string(),
|
|
issuer_npc_id: "npc_scout".to_string(),
|
|
issuer_npc_name: "斥候".to_string(),
|
|
scene_id: None,
|
|
chapter_id: Some("chapter_01".to_string()),
|
|
act_id: None,
|
|
thread_id: None,
|
|
contract_id: None,
|
|
title: "寻找信物".to_string(),
|
|
description: "找回遗失的信物。".to_string(),
|
|
summary: "信物已找到。".to_string(),
|
|
objective: QuestObjectiveSnapshot {
|
|
kind: QuestObjectiveKind::InspectTreasure,
|
|
target_hostile_npc_id: None,
|
|
target_npc_id: None,
|
|
target_scene_id: Some("scene_ruin".to_string()),
|
|
target_item_id: None,
|
|
required_count: 1,
|
|
},
|
|
progress: 1,
|
|
status: QuestStatus::TurnedIn,
|
|
completion_notified: true,
|
|
reward: QuestRewardSnapshot {
|
|
affinity_bonus: 0,
|
|
currency: 0,
|
|
experience: Some(40),
|
|
items: vec![QuestRewardItem {
|
|
item_id: "scout_badge".to_string(),
|
|
category: "trinket".to_string(),
|
|
name: "斥候徽记".to_string(),
|
|
description: None,
|
|
quantity: 1,
|
|
rarity: QuestRewardItemRarity::Rare,
|
|
tags: vec!["badge".to_string()],
|
|
stackable: false,
|
|
stack_key: "scout_badge".to_string(),
|
|
equipment_slot_id: Some(QuestRewardEquipmentSlot::Relic),
|
|
}],
|
|
intel: None,
|
|
story_hint: None,
|
|
},
|
|
reward_text: "获得斥候徽记。".to_string(),
|
|
narrative_binding: QuestNarrativeBindingSnapshot {
|
|
origin: QuestNarrativeOrigin::FallbackBuilder,
|
|
narrative_type: QuestNarrativeType::Retrieval,
|
|
dramatic_need: String::new(),
|
|
issuer_goal: String::new(),
|
|
player_hook: String::new(),
|
|
world_reason: String::new(),
|
|
followup_hooks: Vec::new(),
|
|
},
|
|
steps: Vec::new(),
|
|
active_step_id: None,
|
|
visible_stage: 1,
|
|
hidden_flags: Vec::new(),
|
|
discovered_fact_ids: Vec::new(),
|
|
related_carrier_ids: Vec::new(),
|
|
consequence_ids: Vec::new(),
|
|
created_at_micros: 100,
|
|
updated_at_micros: 300,
|
|
completed_at_micros: Some(200),
|
|
turned_in_at_micros: Some(300),
|
|
});
|
|
|
|
assert_eq!(plan.inventory_mutations.len(), 1);
|
|
assert_eq!(
|
|
plan.progression_grant.as_ref().map(|input| input.amount),
|
|
Some(40)
|
|
);
|
|
assert_eq!(
|
|
plan.chapter_ledger
|
|
.as_ref()
|
|
.map(|input| input.granted_quest_xp),
|
|
Some(40)
|
|
);
|
|
|
|
let module_inventory::InventoryMutation::GrantItem(input) =
|
|
&plan.inventory_mutations[0].mutation
|
|
else {
|
|
panic!("quest turn-in should grant inventory item");
|
|
};
|
|
assert_eq!(input.item.source_reference_id.as_deref(), Some("quest_001"));
|
|
assert_eq!(
|
|
input.item.equipment_slot_id,
|
|
Some(module_inventory::InventoryEquipmentSlot::Relic)
|
|
);
|
|
assert_eq!(
|
|
input.item.source_kind,
|
|
module_inventory::InventoryItemSourceKind::QuestReward
|
|
);
|
|
}
|
|
}
|