use serde::Serialize; use crate::api::models::{CfcTask, EnergyOverview}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub enum PetState { Idle, Happy, Busy, Worried, Excited, Remind, Energetic, Tired, Curious, Sad, Sleeping, Watching, WaitingForInteraction, MiniGame, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct PetStatePayload { pub state: PetState, pub reason: Option, pub data: Option, } /// 核心状态映射。优先级:Sleeping > Excited > Happy > Worried > Busy > Sad > Tired/Energetic/Curious > Idle pub fn map_state( tasks: Option<&[CfcTask]>, energy: Option<&EnergyOverview>, water_done_today: bool, user_inactive: bool, ) -> PetState { let has_pending = tasks .map(|ts| { ts.iter().any(|t| matches!(t.status.as_deref(), Some("PENDING") | Some("IN_PROGRESS"))) }) .unwrap_or(false); if user_inactive && !has_pending { return PetState::Sleeping; } if water_done_today { return PetState::Excited; } if let Some(ts) = tasks { if !ts.is_empty() { if ts.iter().all(|t| t.status.as_deref() == Some("COMPLETED")) { return PetState::Happy; } let today = chrono::Local::now().date_naive(); let overdue = ts.iter().any(|t| { matches!(t.status.as_deref(), Some("PENDING") | Some("IN_PROGRESS")) && t.deadline .as_ref() .and_then(|d| d.to_local_date()) .map(|due| due < today) .unwrap_or(false) }); if overdue { return PetState::Worried; } return PetState::Busy; } } if let Some(e) = energy { if !e.dimensions.is_empty() { let all_low = e.dimensions.iter().all(|d| d.energy < 30); if all_low { return PetState::Sad; } let body = e.dimensions.iter().find(|d| d.code.as_deref() == Some("body")); if let Some(b) = body { if b.energy < 30 { return PetState::Tired; } if b.energy >= 85 { return PetState::Energetic; } } let mind = e.dimensions.iter().find(|d| d.code.as_deref() == Some("wisdom")); if let Some(m) = mind { if m.energy >= 85 { return PetState::Curious; } } } } PetState::Idle } #[cfg(test)] mod tests { use super::*; use crate::api::models::{DimensionValue, FlexDate}; fn task(status: &str, due: Option<&str>) -> CfcTask { CfcTask { id: 1, title: None, status: Some(status.to_string()), deadline: due.map(|s| FlexDate::Iso(s.to_string())), } } fn energy(dims: &[(&str, i32)]) -> EnergyOverview { EnergyOverview { dimensions: dims .iter() .map(|(code, v)| DimensionValue { code: Some(code.to_string()), name: None, icon: None, element: None, energy: *v, health_index: 0, }) .collect(), total_energy: 0, total_health_index: 0, } } #[test] fn sleeping_when_inactive_and_no_pending() { assert_eq!(map_state(Some(&[]), None, false, true), PetState::Sleeping); } #[test] fn not_sleeping_when_inactive_but_pending() { let tasks = vec![task("PENDING", None)]; assert_eq!(map_state(Some(&tasks), None, false, true), PetState::Busy); } #[test] fn excited_when_water_done_today() { let tasks = vec![task("PENDING", None)]; assert_eq!(map_state(Some(&tasks), None, true, false), PetState::Excited); } #[test] fn happy_when_all_completed() { let tasks = vec![task("COMPLETED", None), task("COMPLETED", Some("2026-01-01"))]; assert_eq!(map_state(Some(&tasks), None, false, false), PetState::Happy); } #[test] fn worried_when_overdue() { let tasks = vec![task("PENDING", Some("2000-01-01"))]; assert_eq!(map_state(Some(&tasks), None, false, false), PetState::Worried); } #[test] fn busy_when_in_progress_not_overdue() { let tasks = vec![task("IN_PROGRESS", Some("2099-12-31"))]; assert_eq!(map_state(Some(&tasks), None, false, false), PetState::Busy); } #[test] fn sad_when_all_dimensions_low() { let e = energy(&[("body", 10), ("wisdom", 20), ("mind", 15)]); assert_eq!(map_state(None, Some(&e), false, false), PetState::Sad); } #[test] fn tired_when_body_low() { let e = energy(&[("body", 20), ("wisdom", 90)]); assert_eq!(map_state(None, Some(&e), false, false), PetState::Tired); } #[test] fn energetic_when_body_high() { let e = energy(&[("body", 90)]); assert_eq!(map_state(None, Some(&e), false, false), PetState::Energetic); } #[test] fn curious_when_mind_high() { let e = energy(&[("body", 60), ("wisdom", 88)]); assert_eq!(map_state(None, Some(&e), false, false), PetState::Curious); } #[test] fn idle_when_nothing_qualifies() { assert_eq!(map_state(None, None, false, false), PetState::Idle); } #[test] fn state_serializes_to_camel_case() { let json = serde_json::to_string(&PetState::WaitingForInteraction).unwrap(); assert_eq!(json, "\"waitingForInteraction\""); } }