Selaa lähdekoodia

feat(desktop-pet): 宠物状态映射与优先级测试

liaoxg 1 päivä sitten
vanhempi
sitoutus
36401bcb21
2 muutettua tiedostoa jossa 204 lisäystä ja 0 poistoa
  1. 1 0
      cfc-desktop-pet/src-tauri/src/main.rs
  2. 203 0
      cfc-desktop-pet/src-tauri/src/state.rs

+ 1 - 0
cfc-desktop-pet/src-tauri/src/main.rs

@@ -1,6 +1,7 @@
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 mod api;
+mod state;
 
 fn main() {
     tauri::Builder::default()

+ 203 - 0
cfc-desktop-pet/src-tauri/src/state.rs

@@ -0,0 +1,203 @@
+use serde::Serialize;
+
+use crate::api::models::{CfcTask, DailyCheckin, 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>,
+    checkin: Option<&DailyCheckin>,
+    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 let Some(c) = checkin {
+        if c.checked {
+            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.due_date
+                        .as_deref()
+                        .and_then(|d| {
+                            let head = &d[..d.len().min(10)];
+                            chrono::NaiveDate::parse_from_str(head, "%Y-%m-%d").ok()
+                        })
+                        .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.value < 30);
+            if all_low {
+                return PetState::Sad;
+            }
+            let body = e.dimensions.iter().find(|d| d.dimension.as_deref() == Some("身"));
+            if let Some(b) = body {
+                if b.value < 30 {
+                    return PetState::Tired;
+                }
+                if b.value >= 85 {
+                    return PetState::Energetic;
+                }
+            }
+            let mind = e.dimensions.iter().find(|d| d.dimension.as_deref() == Some("智"));
+            if let Some(m) = mind {
+                if m.value >= 85 {
+                    return PetState::Curious;
+                }
+            }
+        }
+    }
+
+    PetState::Idle
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::api::models::{CfcTask, DailyCheckin, DimensionValue, EnergyOverview};
+
+    fn task(status: &str, due: Option<&str>) -> CfcTask {
+        CfcTask {
+            id: 1,
+            title: None,
+            status: Some(status.to_string()),
+            due_date: due.map(|s| s.to_string()),
+        }
+    }
+
+    fn energy(dims: &[(&str, i32)]) -> EnergyOverview {
+        EnergyOverview {
+            dimensions: dims
+                .iter()
+                .map(|(d, v)| DimensionValue {
+                    dimension: Some(d.to_string()),
+                    value: *v,
+                    color: None,
+                })
+                .collect(),
+        }
+    }
+
+    #[test]
+    fn sleeping_when_inactive_and_no_pending() {
+        assert_eq!(map_state(Some(&[]), None, None, true), PetState::Sleeping);
+    }
+
+    #[test]
+    fn not_sleeping_when_inactive_but_pending() {
+        let tasks = vec![task("PENDING", None)];
+        assert_eq!(map_state(Some(&tasks), None, None, true), PetState::Busy);
+    }
+
+    #[test]
+    fn excited_beats_tasks_when_checked() {
+        let tasks = vec![task("PENDING", None)];
+        let checkin = DailyCheckin { checked: true, items: vec!["water".into()] };
+        assert_eq!(map_state(Some(&tasks), None, Some(&checkin), 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, None, false), PetState::Happy);
+    }
+
+    #[test]
+    fn worried_when_overdue() {
+        let tasks = vec![task("PENDING", Some("2000-01-01"))];
+        assert_eq!(map_state(Some(&tasks), None, None, 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, None, false), PetState::Busy);
+    }
+
+    #[test]
+    fn sad_when_all_dimensions_low() {
+        let e = energy(&[("身", 10), ("智", 20), ("心", 15)]);
+        assert_eq!(map_state(None, Some(&e), None, false), PetState::Sad);
+    }
+
+    #[test]
+    fn tired_when_body_low() {
+        let e = energy(&[("身", 20), ("智", 90)]);
+        assert_eq!(map_state(None, Some(&e), None, false), PetState::Tired);
+    }
+
+    #[test]
+    fn energetic_when_body_high() {
+        let e = energy(&[("身", 90)]);
+        assert_eq!(map_state(None, Some(&e), None, false), PetState::Energetic);
+    }
+
+    #[test]
+    fn curious_when_mind_high() {
+        let e = energy(&[("身", 60), ("智", 88)]);
+        assert_eq!(map_state(None, Some(&e), None, false), PetState::Curious);
+    }
+
+    #[test]
+    fn idle_when_nothing_qualifies() {
+        assert_eq!(map_state(None, None, None, false), PetState::Idle);
+    }
+
+    #[test]
+    fn state_serializes_to_camel_case() {
+        let json = serde_json::to_string(&PetState::WaitingForInteraction).unwrap();
+        assert_eq!(json, "\"waitingForInteraction\"");
+    }
+}