| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- 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<String>,
- pub data: Option<serde_json::Value>,
- }
- /// 核心状态映射。优先级: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\"");
- }
- }
|