219 lines
7.3 KiB
Rust
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);
|
|
}
|
|
}
|