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

219 lines
7.3 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_base_state() -> NpcStateSnapshot {
normalize_npc_state_snapshot(
NpcStateUpsertInput {
runtime_session_id: "runtime_001".to_string(),
npc_id: "npc_001".to_string(),
npc_name: "宁霜".to_string(),
affinity: 18,
help_used: false,
chatted_count: 0,
gifts_given: 0,
recruited: false,
trade_stock_signature: None,
revealed_facts: vec![],
known_attribute_rumors: vec![],
first_meaningful_contact_resolved: false,
seen_backstory_chapter_ids: vec![],
stance_profile: None,
updated_at_micros: 10,
},
None,
)
.expect("base npc state should be valid")
}
#[test]
fn relation_state_uses_expected_thresholds() {
assert_eq!(build_relation_state(-1).stance, NpcRelationStance::Hostile);
assert_eq!(build_relation_state(0).stance, NpcRelationStance::Guarded);
assert_eq!(build_relation_state(15).stance, NpcRelationStance::Neutral);
assert_eq!(
build_relation_state(30).stance,
NpcRelationStance::Cooperative
);
assert_eq!(build_relation_state(60).stance, NpcRelationStance::Bonded);
}
#[test]
fn normalize_npc_state_snapshot_builds_primary_fields() {
let snapshot = build_base_state();
assert_eq!(snapshot.npc_state_id, "npcstate_runtime_001:npc_001");
assert_eq!(snapshot.relation_state.stance, NpcRelationStance::Neutral);
assert_eq!(snapshot.created_at_micros, 10);
assert_eq!(snapshot.updated_at_micros, 10);
}
#[test]
fn chat_action_increases_affinity_and_marks_first_contact() {
let next = apply_npc_social_action(
build_base_state(),
ResolveNpcSocialActionInput {
runtime_session_id: "runtime_001".to_string(),
npc_id: "npc_001".to_string(),
npc_name: "宁霜".to_string(),
action_kind: NpcSocialActionKind::Chat,
affinity_gain_override: None,
note: None,
updated_at_micros: 20,
},
)
.expect("chat should succeed");
assert_eq!(next.affinity, 24);
assert_eq!(next.chatted_count, 1);
assert!(next.first_meaningful_contact_resolved);
assert_eq!(next.updated_at_micros, 20);
}
#[test]
fn help_action_rejects_second_use() {
let used_state = NpcStateSnapshot {
help_used: true,
..build_base_state()
};
let error = apply_npc_social_action(
used_state,
ResolveNpcSocialActionInput {
runtime_session_id: "runtime_001".to_string(),
npc_id: "npc_001".to_string(),
npc_name: "宁霜".to_string(),
action_kind: NpcSocialActionKind::Help,
affinity_gain_override: None,
note: None,
updated_at_micros: 20,
},
)
.expect_err("help should fail once used");
assert_eq!(error, NpcStateFieldError::HelpAlreadyUsed);
}
#[test]
fn recruit_requires_threshold() {
let error = apply_npc_social_action(
build_base_state(),
ResolveNpcSocialActionInput {
runtime_session_id: "runtime_001".to_string(),
npc_id: "npc_001".to_string(),
npc_name: "宁霜".to_string(),
action_kind: NpcSocialActionKind::Recruit,
affinity_gain_override: None,
note: None,
updated_at_micros: 20,
},
)
.expect_err("recruit should require threshold");
assert_eq!(error, NpcStateFieldError::RecruitAffinityTooLow);
}
#[test]
fn recruit_marks_state_when_affinity_is_high_enough() {
let recruitable = NpcStateSnapshot {
affinity: 66,
relation_state: build_relation_state(66),
..build_base_state()
};
let next = apply_npc_social_action(
recruitable,
ResolveNpcSocialActionInput {
runtime_session_id: "runtime_001".to_string(),
npc_id: "npc_001".to_string(),
npc_name: "宁霜".to_string(),
action_kind: NpcSocialActionKind::Recruit,
affinity_gain_override: None,
note: None,
updated_at_micros: 20,
},
)
.expect("recruit should succeed");
assert!(next.recruited);
assert!(next.first_meaningful_contact_resolved);
}
#[test]
fn resolve_preview_talk_keeps_affinity_unchanged() {
let result = resolve_npc_interaction(
build_base_state(),
ResolveNpcInteractionInput {
runtime_session_id: "runtime_001".to_string(),
npc_id: "npc_001".to_string(),
npc_name: "宁霜".to_string(),
interaction_function_id: NPC_PREVIEW_TALK_FUNCTION_ID.to_string(),
release_npc_id: None,
updated_at_micros: 20,
},
)
.expect("preview talk should succeed");
assert_eq!(result.interaction_status, NpcInteractionStatus::Previewed);
assert!(!result.affinity_changed);
assert_eq!(result.previous_affinity, 18);
assert_eq!(result.next_affinity, 18);
}
#[test]
fn resolve_chat_updates_npc_state_and_returns_dialogue_status() {
let result = resolve_npc_interaction(
build_base_state(),
ResolveNpcInteractionInput {
runtime_session_id: "runtime_001".to_string(),
npc_id: "npc_001".to_string(),
npc_name: "宁霜".to_string(),
interaction_function_id: NPC_CHAT_FUNCTION_ID.to_string(),
release_npc_id: None,
updated_at_micros: 20,
},
)
.expect("chat interaction should succeed");
assert_eq!(result.interaction_status, NpcInteractionStatus::Dialogue);
assert!(result.affinity_changed);
assert_eq!(result.next_affinity, 24);
assert!(result.npc_state.first_meaningful_contact_resolved);
}
#[test]
fn resolve_fight_returns_battle_pending_without_affinity_change() {
let result = resolve_npc_interaction(
build_base_state(),
ResolveNpcInteractionInput {
runtime_session_id: "runtime_001".to_string(),
npc_id: "npc_001".to_string(),
npc_name: "宁霜".to_string(),
interaction_function_id: NPC_FIGHT_FUNCTION_ID.to_string(),
release_npc_id: None,
updated_at_micros: 20,
},
)
.expect("fight interaction should succeed");
assert_eq!(
result.interaction_status,
NpcInteractionStatus::BattlePending
);
assert_eq!(result.battle_mode, Some(NpcInteractionBattleMode::Fight));
assert!(!result.affinity_changed);
}
}