cuqueclicker_lib/save/versions/
v3.rs1use serde::{Deserialize, Serialize};
19use std::collections::{HashMap, HashSet};
20
21use super::v2::{BuffV2, FingererStateV2, GameStateV2};
22use crate::game::state::GameState;
23
24fn default_v3_version() -> u32 {
25 3
26}
27
28#[derive(Clone, Serialize, Deserialize)]
29pub struct GameStateV3 {
30 #[serde(default = "default_v3_version")]
31 pub version: u32,
32 #[serde(default)]
33 pub cuques: f64,
34 #[serde(default)]
35 pub total_clicks: u64,
36 #[serde(default)]
37 pub lifetime_cuques: f64,
38 #[serde(default)]
39 pub best_fps: f64,
40 #[serde(default)]
43 pub golden_caught: u64,
44 #[serde(default)]
45 pub lucky_caught: u64,
46 #[serde(default)]
47 pub frenzy_caught: u64,
48 #[serde(default)]
49 pub buff_caught: u64,
50 #[serde(default)]
51 pub green_coin_caught: u64,
52 #[serde(default)]
53 pub fingerers_state: HashMap<String, FingererStateV2>,
54 #[serde(default)]
55 pub achievements_earned: HashSet<String>,
56 #[serde(default)]
57 pub upgrades_earned: HashSet<String>,
58 #[serde(default)]
59 pub prestige: u64,
60 #[serde(default)]
61 pub total_play_ticks: u64,
62 #[serde(default)]
63 pub buffs: Vec<BuffV2>,
64}
65
66impl GameStateV3 {
67 pub fn into_current(self) -> GameState {
73 super::v5::GameStateV5::from(super::v4::GameStateV4::from(self)).into_current()
77 }
78}
79
80impl From<GameStateV2> for GameStateV3 {
83 fn from(v2: GameStateV2) -> Self {
84 GameStateV3 {
85 version: 3,
86 cuques: v2.cuques,
87 total_clicks: v2.total_clicks,
88 lifetime_cuques: v2.lifetime_cuques,
89 best_fps: v2.best_fps,
90 golden_caught: v2.golden_caught,
91 lucky_caught: v2.lucky_caught,
92 frenzy_caught: v2.frenzy_caught,
93 buff_caught: v2.buff_caught,
94 green_coin_caught: v2.green_coin_caught,
95 fingerers_state: v2.fingerers_state,
96 achievements_earned: v2.achievements_earned,
97 upgrades_earned: v2.upgrades_earned,
98 prestige: v2.prestige,
99 total_play_ticks: v2.total_play_ticks,
100 buffs: v2.buffs,
101 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::save::versions::v2::{
111 BuffV2, FingererStateV2, ModifierDurationV2, ModifierEffectV2, ModifierSourceV2, ModifierV2,
112 };
113
114 fn empty_v2_with_pity(pity: u32) -> GameStateV2 {
115 GameStateV2 {
116 version: 2,
117 cuques: 0.0,
118 total_clicks: 0,
119 lifetime_cuques: 0.0,
120 best_fps: 0.0,
121 golden_caught: 0,
122 lucky_caught: 0,
123 frenzy_caught: 0,
124 buff_caught: 0,
125 green_coin_caught: 0,
126 fingerers_state: HashMap::new(),
127 achievements_earned: HashSet::new(),
128 upgrades_earned: HashSet::new(),
129 prestige: 0,
130 total_play_ticks: 0,
131 buffs: vec![],
132 goldens_since_green_coin: pity,
133 }
134 }
135
136 #[test]
137 fn v2_to_v3_drops_pity_counter() {
138 let v2 = empty_v2_with_pity(7);
143 let v3: GameStateV3 = v2.into();
144 assert_eq!(v3.version, 3);
145 let json = serde_json::to_string(&v3).expect("serialize");
148 assert!(
149 !json.contains("goldens_since_green_coin"),
150 "pity counter must not appear in V3 JSON: {json}"
151 );
152 }
153
154 #[test]
155 fn v2_to_v3_preserves_all_other_fields() {
156 let v2 = GameStateV2 {
157 version: 2,
158 cuques: 1234.5,
159 total_clicks: 99,
160 lifetime_cuques: 5_000.0,
161 best_fps: 12.0,
162 golden_caught: 17,
163 lucky_caught: 10,
164 frenzy_caught: 4,
165 buff_caught: 2,
166 green_coin_caught: 1,
167 fingerers_state: [(
168 "index_finger".to_string(),
169 FingererStateV2 {
170 count: 9,
171 modifiers: vec![ModifierV2 {
172 source: ModifierSourceV2::GreenCoin,
173 effects: vec![ModifierEffectV2::AddPercent(0.10)],
174 duration: ModifierDurationV2::Permanent,
175 created_at_tick: 0,
176 }],
177 },
178 )]
179 .into_iter()
180 .collect(),
181 achievements_earned: ["first_finger".into()].into_iter().collect(),
182 upgrades_earned: ["click_mult_1".into()].into_iter().collect(),
183 prestige: 3,
184 total_play_ticks: 9_000,
185 buffs: vec![BuffV2::ClickFrenzy {
186 ticks_remaining: 100,
187 initial_ticks: 260,
188 mult: 777.0,
189 }],
190 goldens_since_green_coin: 5,
191 };
192
193 let v3: GameStateV3 = v2.into();
194
195 assert_eq!(v3.cuques, 1234.5);
196 assert_eq!(v3.total_clicks, 99);
197 assert_eq!(v3.lifetime_cuques, 5_000.0);
198 assert_eq!(v3.best_fps, 12.0);
199 assert_eq!(v3.golden_caught, 17);
200 assert_eq!(v3.lucky_caught, 10);
201 assert_eq!(v3.frenzy_caught, 4);
202 assert_eq!(v3.buff_caught, 2);
203 assert_eq!(v3.green_coin_caught, 1);
204 assert_eq!(v3.prestige, 3);
205 assert_eq!(v3.total_play_ticks, 9_000);
206 assert_eq!(v3.buffs.len(), 1);
207 let st = v3.fingerers_state.get("index_finger").unwrap();
208 assert_eq!(st.count, 9);
209 assert_eq!(st.modifiers.len(), 1);
210 assert!(v3.achievements_earned.contains("first_finger"));
211 assert!(v3.upgrades_earned.contains("click_mult_1"));
212 }
213
214 #[test]
215 fn v3_into_current_zero_inits_powerup_runtime_fields() {
216 let v3 = empty_v2_with_pity(0).into();
224 let live = GameStateV3::into_current(v3);
225 assert!(live.powerups.is_empty());
226 assert_eq!(live.next_spawn_id, 0);
227 for (i, &cd) in live.powerup_cooldowns.iter().enumerate() {
228 assert!(
229 cd > 0,
230 "powerup_cooldowns[{i}] must be seeded non-zero by Default, got {cd}"
231 );
232 }
233 }
234}