|
@@ -0,0 +1,123 @@
|
|
|
|
|
+use serde::{Deserialize, Serialize};
|
|
|
|
|
+use std::collections::HashMap;
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
|
|
|
|
+#[serde(rename_all = "camelCase")]
|
|
|
|
|
+pub enum PetType {
|
|
|
|
|
+ #[default]
|
|
|
|
|
+ Animal,
|
|
|
|
|
+ Human,
|
|
|
|
|
+ Custom,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
+#[serde(rename_all = "snake_case")]
|
|
|
|
|
+pub struct PetConfig {
|
|
|
|
|
+ #[serde(default = "default_version")]
|
|
|
|
|
+ pub version: String,
|
|
|
|
|
+ #[serde(rename = "type", default)]
|
|
|
|
|
+ pub pet_type: PetType,
|
|
|
|
|
+ #[serde(default = "default_species")]
|
|
|
|
|
+ pub species: String,
|
|
|
|
|
+ #[serde(default = "default_gender")]
|
|
|
|
|
+ pub gender: String,
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub image_base_url: String,
|
|
|
|
|
+ #[serde(default = "default_image")]
|
|
|
|
|
+ pub default_image: String,
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub actions: HashMap<String, String>,
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub sounds: HashMap<String, String>,
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub parameters: PetParameters,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
+#[serde(rename_all = "snake_case")]
|
|
|
|
|
+pub struct PetParameters {
|
|
|
|
|
+ #[serde(default = "default_move_distance")]
|
|
|
|
|
+ pub move_distance: i32,
|
|
|
|
|
+ #[serde(default = "default_animation_speed")]
|
|
|
|
|
+ pub animation_speed: f64,
|
|
|
|
|
+ #[serde(default = "default_interaction_timeout")]
|
|
|
|
|
+ pub interaction_timeout: i32,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl Default for PetParameters {
|
|
|
|
|
+ fn default() -> Self {
|
|
|
|
|
+ Self {
|
|
|
|
|
+ move_distance: default_move_distance(),
|
|
|
|
|
+ animation_speed: default_animation_speed(),
|
|
|
|
|
+ interaction_timeout: default_interaction_timeout(),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn default_version() -> String { "1.0".into() }
|
|
|
|
|
+fn default_species() -> String { "other".into() }
|
|
|
|
|
+fn default_gender() -> String { "neutral".into() }
|
|
|
|
|
+fn default_image() -> String { "standing.png".into() }
|
|
|
|
|
+fn default_move_distance() -> i32 { 15 }
|
|
|
|
|
+fn default_animation_speed() -> f64 { 1.0 }
|
|
|
|
|
+fn default_interaction_timeout() -> i32 { 300 }
|
|
|
|
|
+
|
|
|
|
|
+impl PetConfig {
|
|
|
|
|
+ /// 解析 JSON;actions 为空时补默认映射(idle/happy/busy/sleeping)
|
|
|
|
|
+ pub fn parse(json: &str) -> Result<Self, serde_json::Error> {
|
|
|
|
|
+ let mut cfg: PetConfig = serde_json::from_str(json)?;
|
|
|
|
|
+ if cfg.actions.is_empty() {
|
|
|
|
|
+ cfg.actions = [
|
|
|
|
|
+ ("idle".to_string(), "idle".to_string()),
|
|
|
|
|
+ ("happy".to_string(), "happy".to_string()),
|
|
|
|
|
+ ("busy".to_string(), "busy".to_string()),
|
|
|
|
|
+ ("sleeping".to_string(), "sleeping".to_string()),
|
|
|
|
|
+ ]
|
|
|
|
|
+ .into_iter()
|
|
|
|
|
+ .collect();
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(cfg)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pub fn get_action_for_state(&self, state_name: &str) -> String {
|
|
|
|
|
+ self.actions.get(state_name).cloned().unwrap_or_else(|| state_name.to_string())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[cfg(test)]
|
|
|
|
|
+mod tests {
|
|
|
|
|
+ use super::*;
|
|
|
|
|
+
|
|
|
|
|
+ const XIBAO_JSON: &str = include_str!("../../src/DefaultPets/xibao/pet-config.json");
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn parses_real_xibao_config() {
|
|
|
|
|
+ let cfg = PetConfig::parse(XIBAO_JSON).unwrap();
|
|
|
|
|
+ assert_eq!(cfg.version, "1.0");
|
|
|
|
|
+ assert!(matches!(cfg.pet_type, PetType::Animal));
|
|
|
|
|
+ assert_eq!(cfg.image_base_url, "DefaultPets/xibao/");
|
|
|
|
|
+ assert_eq!(cfg.default_image, "standing.png");
|
|
|
|
|
+ assert!(cfg.actions.contains_key("idle"));
|
|
|
|
|
+ assert_eq!(cfg.get_action_for_state("happy"), "wings_spread");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn fills_default_actions_when_empty() {
|
|
|
|
|
+ let json = r#"{"version":"1.0","type":"custom","actions":{}}"#;
|
|
|
|
|
+ let cfg = PetConfig::parse(json).unwrap();
|
|
|
|
|
+ assert_eq!(cfg.actions.len(), 4);
|
|
|
|
|
+ assert_eq!(cfg.actions["idle"], "idle");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn action_falls_back_to_state_name() {
|
|
|
|
|
+ let json = r#"{"type":"custom","actions":{"idle":"idle"}}"#;
|
|
|
|
|
+ let cfg = PetConfig::parse(json).unwrap();
|
|
|
|
|
+ assert_eq!(cfg.get_action_for_state("no_such_state"), "no_such_state");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn parse_error_on_bad_json() {
|
|
|
|
|
+ assert!(PetConfig::parse("not json").is_err());
|
|
|
|
|
+ }
|
|
|
|
|
+}
|