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::*; fn build_test_reward() -> QuestRewardSnapshot { QuestRewardSnapshot { affinity_bonus: 12, currency: 72, experience: Some(35), items: vec![QuestRewardItem { item_id: "reward_item_01".to_string(), category: "补给".to_string(), name: "常备药包".to_string(), description: Some("带着草药苦味的便携补给。".to_string()), quantity: 1, rarity: QuestRewardItemRarity::Rare, tags: vec!["healing".to_string()], stackable: true, stack_key: "reward_item_01".to_string(), equipment_slot_id: None, }], intel: Some(QuestRewardIntel { rumor_text: "旧桥下面还埋着另一条线索。".to_string(), unlocked_scene_id: Some("scene_old_bridge".to_string()), }), story_hint: Some("委托人的态度明显缓和了下来。".to_string()), } } fn build_test_binding() -> QuestNarrativeBindingSnapshot { QuestNarrativeBindingSnapshot { origin: QuestNarrativeOrigin::FallbackBuilder, narrative_type: QuestNarrativeType::Investigation, dramatic_need: "委托人需要先确认遗迹异动是真是假。".to_string(), issuer_goal: "摸清遗迹周边的情况。".to_string(), player_hook: "玩家正好就在现场。".to_string(), world_reason: "最近的异常都收束到了这片区域。".to_string(), followup_hooks: vec!["遗迹后方还有更深的入口".to_string()], } } fn build_test_steps() -> Vec { vec![ QuestStepSnapshot { step_id: "step_investigate".to_string(), kind: QuestObjectiveKind::InspectTreasure, target_hostile_npc_id: None, target_npc_id: None, target_scene_id: Some("scene_ruins".to_string()), target_item_id: None, required_count: 1, progress: 0, title: "调查遗迹".to_string(), reveal_text: "先去把遗迹边缘的异动看清楚。".to_string(), complete_text: "遗迹调查已经完成。".to_string(), }, QuestStepSnapshot { step_id: "step_report_back".to_string(), kind: QuestObjectiveKind::TalkToNpc, target_hostile_npc_id: None, target_npc_id: Some("npc_scholar_lin".to_string()), target_scene_id: None, target_item_id: None, required_count: 1, progress: 0, title: "回去汇报".to_string(), reveal_text: "回去把结果告诉林朔。".to_string(), complete_text: "林朔已经收到了你的回报。".to_string(), }, ] } fn build_test_record() -> QuestRecordSnapshot { build_quest_record_snapshot(QuestRecordInput { quest_id: "quest:npc_scholar_lin:inspect_treasure:scene_ruins".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_scholar_lin".to_string(), issuer_npc_name: "林朔".to_string(), scene_id: Some("scene_ruins".to_string()), chapter_id: Some("chapter_01".to_string()), act_id: Some("act_01".to_string()), thread_id: Some("thread_ruins".to_string()), contract_id: Some("contract_01".to_string()), title: "遗迹异动".to_string(), description: "林朔希望你先去确认遗迹外缘的异常。".to_string(), summary: "调查遗迹异动,再回去汇报".to_string(), status: QuestStatus::Active, completion_notified: false, reward: build_test_reward(), reward_text: "完成后可获得赏金、补给和线索。".to_string(), narrative_binding: build_test_binding(), steps: build_test_steps(), active_step_id: Some("step_investigate".to_string()), visible_stage: 0, hidden_flags: vec!["ruins".to_string()], discovered_fact_ids: vec![], related_carrier_ids: vec![], consequence_ids: vec![], created_at_micros: 1_713_680_000_000_000, }) .expect("test quest record should build") } #[test] fn build_quest_record_snapshot_rejects_empty_steps() { let error = build_quest_record_snapshot(QuestRecordInput { quest_id: "quest_001".to_string(), runtime_session_id: "runtime_001".to_string(), story_session_id: None, actor_user_id: "user_001".to_string(), issuer_npc_id: "npc_001".to_string(), issuer_npc_name: "林朔".to_string(), scene_id: None, chapter_id: None, act_id: None, thread_id: None, contract_id: None, title: "遗迹异动".to_string(), description: "测试".to_string(), summary: String::new(), status: QuestStatus::Active, completion_notified: false, reward: build_test_reward(), reward_text: "完成后可获得赏金。".to_string(), narrative_binding: build_test_binding(), steps: vec![], active_step_id: None, visible_stage: 0, hidden_flags: vec![], discovered_fact_ids: vec![], related_carrier_ids: vec![], consequence_ids: vec![], created_at_micros: 1, }) .expect_err("empty steps should fail"); assert_eq!(error, QuestRecordFieldError::EmptySteps); } #[test] fn apply_quest_signal_advances_only_current_active_step() { let current = build_test_record(); let outcome = apply_quest_signal( current, QuestSignalApplyInput { quest_id: "quest:npc_scholar_lin:inspect_treasure:scene_ruins".to_string(), signal: QuestProgressSignal::TreasureInspected(QuestTreasureInspectedSignal { scene_id: Some("scene_ruins".to_string()), }), updated_at_micros: 1_713_680_000_100_000, }, ) .expect("signal apply should succeed"); assert!(outcome.changed); assert!(!outcome.completed_now); assert_eq!(outcome.next_record.status, QuestStatus::Active); assert_eq!( outcome.next_record.active_step_id.as_deref(), Some("step_report_back") ); assert_eq!(outcome.changed_step_id.as_deref(), Some("step_investigate")); assert_eq!(outcome.changed_step_progress, Some(1)); } #[test] fn apply_quest_signal_marks_completed_when_last_step_finishes() { let current = apply_quest_signal( build_test_record(), QuestSignalApplyInput { quest_id: "quest:npc_scholar_lin:inspect_treasure:scene_ruins".to_string(), signal: QuestProgressSignal::TreasureInspected(QuestTreasureInspectedSignal { scene_id: Some("scene_ruins".to_string()), }), updated_at_micros: 20, }, ) .expect("first step should succeed") .next_record; let outcome = apply_quest_signal( current, QuestSignalApplyInput { quest_id: "quest:npc_scholar_lin:inspect_treasure:scene_ruins".to_string(), signal: QuestProgressSignal::NpcTalkCompleted(QuestNpcTalkCompletedSignal { npc_id: "npc_scholar_lin".to_string(), }), updated_at_micros: 30, }, ) .expect("last step should complete"); assert!(outcome.changed); assert!(outcome.completed_now); assert_eq!(outcome.next_record.status, QuestStatus::Completed); assert_eq!(outcome.next_record.active_step_id, None); assert_eq!(outcome.next_record.completed_at_micros, Some(30)); } #[test] fn turn_in_quest_record_moves_status_to_turned_in() { let completed = apply_quest_signal( apply_quest_signal( build_test_record(), QuestSignalApplyInput { quest_id: "quest:npc_scholar_lin:inspect_treasure:scene_ruins".to_string(), signal: QuestProgressSignal::TreasureInspected(QuestTreasureInspectedSignal { scene_id: Some("scene_ruins".to_string()), }), updated_at_micros: 20, }, ) .expect("first step should succeed") .next_record, QuestSignalApplyInput { quest_id: "quest:npc_scholar_lin:inspect_treasure:scene_ruins".to_string(), signal: QuestProgressSignal::NpcTalkCompleted(QuestNpcTalkCompletedSignal { npc_id: "npc_scholar_lin".to_string(), }), updated_at_micros: 30, }, ) .expect("second step should succeed") .next_record; let turned_in = turn_in_quest_record( completed, QuestTurnInInput { quest_id: "quest:npc_scholar_lin:inspect_treasure:scene_ruins".to_string(), turned_in_at_micros: 40, }, ) .expect("completed quest should turn in"); assert_eq!(turned_in.status, QuestStatus::TurnedIn); assert_eq!(turned_in.turned_in_at_micros, Some(40)); assert!(turned_in.completion_notified); assert!( turned_in .steps .iter() .all(|step| step.progress == step.required_count) ); } }