use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum BarkBattleDifficultyPreset { Easy, Normal, Hard, } impl Default for BarkBattleDifficultyPreset { fn default() -> Self { Self::Normal } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum BarkBattleServerResult { PlayerWin, OpponentWin, Draw, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum BarkBattleFinishStatus { Accepted, AcceptedWithFlags, Rejected, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleConfigEditorPayload { pub title: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub theme_preset: String, pub player_dog_skin_preset: String, pub opponent_dog_skin_preset: String, #[serde(default)] pub difficulty_preset: BarkBattleDifficultyPreset, pub leaderboard_enabled: bool, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleDraftCreateRequest { pub title: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub theme_preset: String, pub player_dog_skin_preset: String, pub opponent_dog_skin_preset: String, #[serde(default)] pub difficulty_preset: BarkBattleDifficultyPreset, pub leaderboard_enabled: bool, } impl From for BarkBattleConfigEditorPayload { fn from(value: BarkBattleDraftCreateRequest) -> Self { Self { title: value.title, description: value.description, theme_preset: value.theme_preset, player_dog_skin_preset: value.player_dog_skin_preset, opponent_dog_skin_preset: value.opponent_dog_skin_preset, difficulty_preset: value.difficulty_preset, leaderboard_enabled: value.leaderboard_enabled, } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleWorkPublishRequest { pub draft_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub work_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub published_snapshot: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleDraftConfig { pub draft_id: String, pub title: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub theme_preset: String, pub player_dog_skin_preset: String, pub opponent_dog_skin_preset: String, #[serde(default)] pub difficulty_preset: BarkBattleDifficultyPreset, pub leaderboard_enabled: bool, pub updated_at: String, } impl Default for BarkBattleDraftConfig { fn default() -> Self { Self { draft_id: String::new(), title: String::new(), description: None, theme_preset: String::new(), player_dog_skin_preset: String::new(), opponent_dog_skin_preset: String::new(), difficulty_preset: BarkBattleDifficultyPreset::Normal, leaderboard_enabled: true, updated_at: String::new(), } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattlePublishedConfig { pub work_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub draft_id: Option, pub config_version: u32, pub ruleset_version: String, pub play_type_id: String, pub title: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub theme_preset: String, pub player_dog_skin_preset: String, pub opponent_dog_skin_preset: String, pub difficulty_preset: BarkBattleDifficultyPreset, pub leaderboard_enabled: bool, pub updated_at: String, pub published_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleRuntimeConfig { pub work_id: String, pub config_version: u32, pub ruleset_version: String, pub play_type_id: String, pub duration_ms: u64, pub energy_min: f32, pub energy_max: f32, pub draw_threshold: f32, pub min_bark_gap_ms: u64, pub difficulty_preset: BarkBattleDifficultyPreset, pub theme_preset: String, pub player_dog_skin_preset: String, pub opponent_dog_skin_preset: String, pub leaderboard_enabled: bool, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleRunStartRequest { pub work_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub source_route: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub client_runtime_version: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleRunStartResponse { pub run_id: String, pub run_token: String, pub work_id: String, pub config_version: u32, pub ruleset_version: String, pub difficulty_preset: BarkBattleDifficultyPreset, pub runtime_config: BarkBattleRuntimeConfig, pub server_started_at: String, pub expires_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleDerivedMetrics { pub trigger_count: u32, pub max_volume: f32, pub average_volume: f32, pub final_energy: f32, pub combo_max: u32, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleRunFinishRequest { pub work_id: String, pub run_id: String, pub run_token: String, pub config_version: u32, pub ruleset_version: String, pub difficulty_preset: BarkBattleDifficultyPreset, pub client_started_at: String, pub client_finished_at: String, pub duration_ms: u64, pub derived_metrics: BarkBattleDerivedMetrics, #[serde(default, skip_serializing_if = "Option::is_none")] pub client_result: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sample_digest: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub client_runtime_version: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleScoreSummary { pub duration_ms: u64, pub trigger_count: u32, pub max_volume: f32, pub average_volume: f32, pub final_energy: f32, pub combo_max: u32, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleRunFinishResponse { pub status: BarkBattleFinishStatus, pub run_id: String, pub work_id: String, pub config_version: u32, pub ruleset_version: String, pub difficulty_preset: BarkBattleDifficultyPreset, pub server_result: BarkBattleServerResult, pub score_summary: BarkBattleScoreSummary, #[serde(default, skip_serializing_if = "Option::is_none")] pub leaderboard_score: Option, #[serde(default)] pub anti_cheat_flags: Vec, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleLeaderboardEntry { pub rank: u32, pub run_id: String, pub work_id: String, pub config_version: u32, pub ruleset_version: String, pub difficulty_preset: BarkBattleDifficultyPreset, pub display_name: String, pub server_result: BarkBattleServerResult, pub score_summary: BarkBattleScoreSummary, pub leaderboard_score: u64, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleLeaderboardResponse { pub work_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_version: Option, pub ruleset_version: String, pub difficulty_preset: BarkBattleDifficultyPreset, #[serde(default)] pub entries: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub viewer_best: Option, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattlePersonalHistoryItem { pub run_id: String, pub work_id: String, pub config_version: u32, pub ruleset_version: String, pub difficulty_preset: BarkBattleDifficultyPreset, pub server_result: BarkBattleServerResult, pub score_summary: BarkBattleScoreSummary, #[serde(default, skip_serializing_if = "Option::is_none")] pub leaderboard_score: Option, #[serde(default)] pub anti_cheat_flags: Vec, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattlePersonalBestSummary { pub work_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_version: Option, pub ruleset_version: String, pub difficulty_preset: BarkBattleDifficultyPreset, #[serde(default, skip_serializing_if = "Option::is_none")] pub best_leaderboard_score: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub best_final_energy: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub best_trigger_count: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub best_max_volume: Option, pub win_count: u64, pub draw_count: u64, pub loss_count: u64, pub finish_count: u64, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattlePersonalHistoryResponse { #[serde(default, skip_serializing_if = "Option::is_none")] pub work_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub difficulty_preset: Option, #[serde(default)] pub items: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub best_summary: Option, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BarkBattleWorkStats { pub work_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_version: Option, pub ruleset_version: String, pub difficulty_preset: BarkBattleDifficultyPreset, pub play_start_count: u64, pub finish_count: u64, pub win_count: u64, pub draw_count: u64, pub loss_count: u64, pub flagged_count: u64, pub leaderboard_entry_count: u64, #[serde(default, skip_serializing_if = "Option::is_none")] pub best_leaderboard_score: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub best_final_energy: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub average_final_energy: Option, pub updated_at: String, } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn draft_config_defaults_to_normal_difficulty() { let config = BarkBattleDraftConfig::default(); assert_eq!(config.difficulty_preset, BarkBattleDifficultyPreset::Normal); } #[test] fn run_requests_carry_config_and_ruleset_identity() { let start = BarkBattleRunStartRequest { work_id: "work-1".to_string(), config_version: Some(3), source_route: Some("gallery".to_string()), client_runtime_version: Some("runtime-v1".to_string()), }; let start_payload = serde_json::to_value(start).expect("start request should serialize"); assert_eq!(start_payload["workId"], json!("work-1")); assert_eq!(start_payload["configVersion"], json!(3)); assert_eq!(start_payload["sourceRoute"], json!("gallery")); let finish = BarkBattleRunFinishRequest { work_id: "work-1".to_string(), run_id: "run-1".to_string(), run_token: "token-1".to_string(), config_version: 3, ruleset_version: "bark-battle-ruleset-v1".to_string(), difficulty_preset: BarkBattleDifficultyPreset::Hard, client_started_at: "2026-05-13T11:00:00Z".to_string(), client_finished_at: "2026-05-13T11:00:30Z".to_string(), duration_ms: 30_000, derived_metrics: BarkBattleDerivedMetrics { trigger_count: 12, max_volume: 0.95, average_volume: 0.62, final_energy: 88.5, combo_max: 7, }, client_result: Some(BarkBattleServerResult::PlayerWin), sample_digest: Some("digest-1".to_string()), client_runtime_version: None, }; let finish_payload = serde_json::to_value(finish).expect("finish request should serialize"); assert_eq!(finish_payload["configVersion"], json!(3)); assert_eq!( finish_payload["rulesetVersion"], json!("bark-battle-ruleset-v1") ); assert_eq!(finish_payload["difficultyPreset"], json!("hard")); } #[test] fn optional_fields_are_omitted_when_absent() { let draft = BarkBattleDraftConfig::default(); let payload = serde_json::to_value(draft).expect("draft should serialize"); assert!(!payload.as_object().unwrap().contains_key("description")); let response = BarkBattlePersonalHistoryResponse { work_id: None, difficulty_preset: None, items: Vec::new(), best_summary: None, updated_at: "2026-05-13T11:00:00Z".to_string(), }; let payload = serde_json::to_value(response).expect("history response should serialize"); assert!(!payload.as_object().unwrap().contains_key("workId")); assert!( !payload .as_object() .unwrap() .contains_key("difficultyPreset") ); assert!(!payload.as_object().unwrap().contains_key("bestSummary")); } #[test] fn finish_response_serializes_player_win_and_accepted() { let response = BarkBattleRunFinishResponse { status: BarkBattleFinishStatus::Accepted, run_id: "run-1".to_string(), work_id: "work-1".to_string(), config_version: 3, ruleset_version: "bark-battle-ruleset-v1".to_string(), difficulty_preset: BarkBattleDifficultyPreset::Normal, server_result: BarkBattleServerResult::PlayerWin, score_summary: BarkBattleScoreSummary { duration_ms: 30_000, trigger_count: 12, max_volume: 0.95, average_volume: 0.62, final_energy: 88.5, combo_max: 7, }, leaderboard_score: Some(98_765), anti_cheat_flags: Vec::new(), updated_at: "2026-05-13T11:00:00Z".to_string(), }; let payload = serde_json::to_value(response).expect("finish response should serialize"); assert_eq!(payload["runId"], json!("run-1")); assert_eq!(payload["status"], json!("accepted")); assert_eq!(payload["serverResult"], json!("player_win")); assert_eq!(payload["leaderboardScore"], json!(98_765)); assert_eq!(payload["scoreSummary"]["finalEnergy"], json!(88.5)); } #[test] fn work_stats_fields_are_constructible() { let stats = BarkBattleWorkStats { work_id: "work-1".to_string(), config_version: Some(3), ruleset_version: "bark-battle-ruleset-v1".to_string(), difficulty_preset: BarkBattleDifficultyPreset::Normal, play_start_count: 10, finish_count: 9, win_count: 5, draw_count: 2, loss_count: 2, flagged_count: 1, leaderboard_entry_count: 4, best_leaderboard_score: Some(98_765), best_final_energy: Some(97.5), average_final_energy: Some(73.25), updated_at: "2026-05-13T11:00:00Z".to_string(), }; assert_eq!(stats.work_id, "work-1"); assert_eq!(stats.play_start_count, 10); assert_eq!(stats.finish_count, 9); assert_eq!(stats.win_count, 5); assert_eq!(stats.draw_count, 2); assert_eq!(stats.loss_count, 2); assert_eq!(stats.flagged_count, 1); assert_eq!(stats.leaderboard_entry_count, 4); assert_eq!(stats.best_leaderboard_score, Some(98_765)); assert_eq!(stats.best_final_energy, Some(97.5)); assert_eq!(stats.average_final_energy, Some(73.25)); assert_eq!(stats.updated_at, "2026-05-13T11:00:00Z"); } }