config.rs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. use serde::{Deserialize, Serialize};
  2. use std::collections::HashMap;
  3. #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
  4. #[serde(rename_all = "camelCase")]
  5. pub enum PetType {
  6. #[default]
  7. Animal,
  8. Human,
  9. Custom,
  10. }
  11. #[derive(Debug, Clone, Serialize, Deserialize)]
  12. #[serde(rename_all = "snake_case")]
  13. pub struct PetConfig {
  14. #[serde(default = "default_version")]
  15. pub version: String,
  16. #[serde(rename = "type", default)]
  17. pub pet_type: PetType,
  18. #[serde(default = "default_species")]
  19. pub species: String,
  20. #[serde(default = "default_gender")]
  21. pub gender: String,
  22. #[serde(default)]
  23. pub image_base_url: String,
  24. #[serde(default = "default_image")]
  25. pub default_image: String,
  26. #[serde(default)]
  27. pub actions: HashMap<String, String>,
  28. /// 动作名 → 帧序列剪辑(真运动序列帧)。provider 无关:
  29. /// frames[] 为逐帧图片(相对 image_base_url 或绝对 URL);sprite 为图集(可选)。
  30. #[serde(default)]
  31. pub animations: HashMap<String, ActionClip>,
  32. #[serde(default)]
  33. pub sounds: HashMap<String, String>,
  34. #[serde(default)]
  35. pub parameters: PetParameters,
  36. }
  37. #[derive(Debug, Clone, Serialize, Deserialize)]
  38. #[serde(rename_all = "snake_case")]
  39. pub struct PetParameters {
  40. #[serde(default = "default_move_distance")]
  41. pub move_distance: i32,
  42. #[serde(default = "default_animation_speed")]
  43. pub animation_speed: f64,
  44. #[serde(default = "default_interaction_timeout")]
  45. pub interaction_timeout: i32,
  46. }
  47. impl Default for PetParameters {
  48. fn default() -> Self {
  49. Self {
  50. move_distance: default_move_distance(),
  51. animation_speed: default_animation_speed(),
  52. interaction_timeout: default_interaction_timeout(),
  53. }
  54. }
  55. }
  56. /// 单个动作的帧序列剪辑(provider 无关的动作动画基础)。
  57. #[derive(Debug, Clone, Serialize, Deserialize)]
  58. #[serde(rename_all = "snake_case")]
  59. pub struct ActionClip {
  60. /// 逐帧图片列表(8-16 帧)。相对路径以 image_base_url 为前缀。
  61. #[serde(default)]
  62. pub frames: Vec<String>,
  63. /// 图集图片(可选,优先级低于 frames)。
  64. #[serde(default)]
  65. pub sprite: Option<String>,
  66. #[serde(default = "default_frame_width")]
  67. pub frame_width: u32,
  68. #[serde(default = "default_frame_height")]
  69. pub frame_height: u32,
  70. #[serde(default)]
  71. pub count: u32,
  72. #[serde(default = "default_fps")]
  73. pub fps: f64,
  74. #[serde(default = "default_true")]
  75. pub r#loop: bool,
  76. }
  77. fn default_version() -> String { "1.0".into() }
  78. fn default_species() -> String { "other".into() }
  79. fn default_gender() -> String { "neutral".into() }
  80. fn default_image() -> String { "standing.png".into() }
  81. fn default_move_distance() -> i32 { 15 }
  82. fn default_animation_speed() -> f64 { 1.0 }
  83. fn default_interaction_timeout() -> i32 { 300 }
  84. fn default_frame_width() -> u32 { 128 }
  85. fn default_frame_height() -> u32 { 128 }
  86. fn default_fps() -> f64 { 12.0 }
  87. fn default_true() -> bool { true }
  88. impl PetConfig {
  89. /// 解析 JSON;actions 为空时补默认映射(idle/happy/busy/sleeping)
  90. pub fn parse(json: &str) -> Result<Self, serde_json::Error> {
  91. let mut cfg: PetConfig = serde_json::from_str(json)?;
  92. if cfg.actions.is_empty() {
  93. cfg.actions = [
  94. ("idle".to_string(), "idle".to_string()),
  95. ("happy".to_string(), "happy".to_string()),
  96. ("busy".to_string(), "busy".to_string()),
  97. ("sleeping".to_string(), "sleeping".to_string()),
  98. ]
  99. .into_iter()
  100. .collect();
  101. }
  102. Ok(cfg)
  103. }
  104. pub fn get_action_for_state(&self, state_name: &str) -> String {
  105. self.actions.get(state_name).cloned().unwrap_or_else(|| state_name.to_string())
  106. }
  107. /// 解析某状态对应的帧序列剪辑(provider 无关)。无则 None(前端回退 CSS 动画)。
  108. pub fn get_clip_for_state(&self, state_name: &str) -> Option<&ActionClip> {
  109. let action = self.get_action_for_state(state_name);
  110. self.animations.get(&action)
  111. }
  112. }
  113. #[cfg(test)]
  114. mod tests {
  115. use super::*;
  116. const XIBAO_JSON: &str = include_str!("../../src/DefaultPets/xibao/pet-config.json");
  117. #[test]
  118. fn parses_real_xibao_config() {
  119. let cfg = PetConfig::parse(XIBAO_JSON).unwrap();
  120. assert_eq!(cfg.version, "1.0");
  121. assert!(matches!(cfg.pet_type, PetType::Animal));
  122. assert_eq!(cfg.image_base_url, "DefaultPets/xibao/");
  123. assert_eq!(cfg.default_image, "standing.png");
  124. assert!(cfg.actions.contains_key("idle"));
  125. assert_eq!(cfg.get_action_for_state("happy"), "wings_spread");
  126. }
  127. #[test]
  128. fn fills_default_actions_when_empty() {
  129. let json = r#"{"version":"1.0","type":"custom","actions":{}}"#;
  130. let cfg = PetConfig::parse(json).unwrap();
  131. assert_eq!(cfg.actions.len(), 4);
  132. assert_eq!(cfg.actions["idle"], "idle");
  133. }
  134. #[test]
  135. fn action_falls_back_to_state_name() {
  136. let json = r#"{"type":"custom","actions":{"idle":"idle"}}"#;
  137. let cfg = PetConfig::parse(json).unwrap();
  138. assert_eq!(cfg.get_action_for_state("no_such_state"), "no_such_state");
  139. }
  140. #[test]
  141. fn parse_error_on_bad_json() {
  142. assert!(PetConfig::parse("not json").is_err());
  143. }
  144. #[test]
  145. fn parses_action_frames_and_resolves_by_state() {
  146. let json = r#"{"type":"custom","actions":{"happy":"cheer"},"animations":{"cheer":{"frames":["a_01.png","a_02.png","a_03.png"],"fps":15,"loop":false}}}"#;
  147. let cfg = PetConfig::parse(json).unwrap();
  148. let clip = cfg.get_clip_for_state("happy").expect("clip");
  149. assert_eq!(clip.frames.len(), 3);
  150. assert_eq!(clip.fps, 15.0);
  151. assert!(!clip.r#loop);
  152. assert_eq!(clip.frame_width, 128);
  153. assert_eq!(clip.frame_height, 128);
  154. assert_eq!(clip.count, 0);
  155. }
  156. #[test]
  157. fn no_clip_when_animations_absent() {
  158. let json = r#"{"type":"custom","actions":{"idle":"idle"}}"#;
  159. let cfg = PetConfig::parse(json).unwrap();
  160. assert!(cfg.get_clip_for_state("idle").is_none());
  161. assert!(cfg.animations.is_empty());
  162. }
  163. #[test]
  164. fn action_clip_loop_defaults_true() {
  165. let json = r#"{"type":"custom","actions":{"idle":"idle"},"animations":{"idle":{"frames":["x.png"]}}}"#;
  166. let cfg = PetConfig::parse(json).unwrap();
  167. assert!(cfg.get_clip_for_state("idle").unwrap().r#loop);
  168. }
  169. }