Files
Genarrative/server-rs/crates/module-quest/src/lib.rs

260 lines
10 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::*;
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<QuestStepSnapshot> {
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)
);
}
}