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 let fingerers_state = self
74 .fingerers_state
75 .into_iter()
76 .map(|(id, st)| (id, st.into()))
77 .collect();
78 let buffs = self.buffs.into_iter().map(Into::into).collect();
79 GameState {
80 version: crate::save::CURRENT_VERSION,
81 cuques: self.cuques,
82 total_clicks: self.total_clicks,
83 lifetime_cuques: self.lifetime_cuques,
84 best_fps: self.best_fps,
85 golden_caught: self.golden_caught,
86 lucky_caught: self.lucky_caught,
87 frenzy_caught: self.frenzy_caught,
88 buff_caught: self.buff_caught,
89 green_coin_caught: self.green_coin_caught,
90 fingerers_state,
91 achievements_earned: self.achievements_earned,
92 upgrades_earned: self.upgrades_earned,
93 prestige: self.prestige,
94 total_play_ticks: self.total_play_ticks,
95 buffs,
96 ..GameState::default()
97 }
98 }
99}
100
101impl From<GameStateV2> for GameStateV3 {
104 fn from(v2: GameStateV2) -> Self {
105 GameStateV3 {
106 version: 3,
107 cuques: v2.cuques,
108 total_clicks: v2.total_clicks,
109 lifetime_cuques: v2.lifetime_cuques,
110 best_fps: v2.best_fps,
111 golden_caught: v2.golden_caught,
112 lucky_caught: v2.lucky_caught,
113 frenzy_caught: v2.frenzy_caught,
114 buff_caught: v2.buff_caught,
115 green_coin_caught: v2.green_coin_caught,
116 fingerers_state: v2.fingerers_state,
117 achievements_earned: v2.achievements_earned,
118 upgrades_earned: v2.upgrades_earned,
119 prestige: v2.prestige,
120 total_play_ticks: v2.total_play_ticks,
121 buffs: v2.buffs,
122 }
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::save::versions::v2::{
132 BuffV2, FingererStateV2, ModifierDurationV2, ModifierEffectV2, ModifierSourceV2, ModifierV2,
133 };
134
135 fn empty_v2_with_pity(pity: u32) -> GameStateV2 {
136 GameStateV2 {
137 version: 2,
138 cuques: 0.0,
139 total_clicks: 0,
140 lifetime_cuques: 0.0,
141 best_fps: 0.0,
142 golden_caught: 0,
143 lucky_caught: 0,
144 frenzy_caught: 0,
145 buff_caught: 0,
146 green_coin_caught: 0,
147 fingerers_state: HashMap::new(),
148 achievements_earned: HashSet::new(),
149 upgrades_earned: HashSet::new(),
150 prestige: 0,
151 total_play_ticks: 0,
152 buffs: vec![],
153 goldens_since_green_coin: pity,
154 }
155 }
156
157 #[test]
158 fn v2_to_v3_drops_pity_counter() {
159 let v2 = empty_v2_with_pity(7);
164 let v3: GameStateV3 = v2.into();
165 assert_eq!(v3.version, 3);
166 let json = serde_json::to_string(&v3).expect("serialize");
169 assert!(
170 !json.contains("goldens_since_green_coin"),
171 "pity counter must not appear in V3 JSON: {json}"
172 );
173 }
174
175 #[test]
176 fn v2_to_v3_preserves_all_other_fields() {
177 let v2 = GameStateV2 {
178 version: 2,
179 cuques: 1234.5,
180 total_clicks: 99,
181 lifetime_cuques: 5_000.0,
182 best_fps: 12.0,
183 golden_caught: 17,
184 lucky_caught: 10,
185 frenzy_caught: 4,
186 buff_caught: 2,
187 green_coin_caught: 1,
188 fingerers_state: [(
189 "index_finger".to_string(),
190 FingererStateV2 {
191 count: 9,
192 modifiers: vec![ModifierV2 {
193 source: ModifierSourceV2::GreenCoin,
194 effects: vec![ModifierEffectV2::AddPercent(0.10)],
195 duration: ModifierDurationV2::Permanent,
196 created_at_tick: 0,
197 }],
198 },
199 )]
200 .into_iter()
201 .collect(),
202 achievements_earned: ["first_finger".into()].into_iter().collect(),
203 upgrades_earned: ["click_mult_1".into()].into_iter().collect(),
204 prestige: 3,
205 total_play_ticks: 9_000,
206 buffs: vec![BuffV2::ClickFrenzy {
207 ticks_remaining: 100,
208 initial_ticks: 260,
209 mult: 777.0,
210 }],
211 goldens_since_green_coin: 5,
212 };
213
214 let v3: GameStateV3 = v2.into();
215
216 assert_eq!(v3.cuques, 1234.5);
217 assert_eq!(v3.total_clicks, 99);
218 assert_eq!(v3.lifetime_cuques, 5_000.0);
219 assert_eq!(v3.best_fps, 12.0);
220 assert_eq!(v3.golden_caught, 17);
221 assert_eq!(v3.lucky_caught, 10);
222 assert_eq!(v3.frenzy_caught, 4);
223 assert_eq!(v3.buff_caught, 2);
224 assert_eq!(v3.green_coin_caught, 1);
225 assert_eq!(v3.prestige, 3);
226 assert_eq!(v3.total_play_ticks, 9_000);
227 assert_eq!(v3.buffs.len(), 1);
228 let st = v3.fingerers_state.get("index_finger").unwrap();
229 assert_eq!(st.count, 9);
230 assert_eq!(st.modifiers.len(), 1);
231 assert!(v3.achievements_earned.contains("first_finger"));
232 assert!(v3.upgrades_earned.contains("click_mult_1"));
233 }
234
235 #[test]
236 fn v3_into_current_zero_inits_powerup_runtime_fields() {
237 let v3 = empty_v2_with_pity(0).into();
245 let live = GameStateV3::into_current(v3);
246 assert!(live.powerups.is_empty());
247 assert_eq!(live.next_spawn_id, 0);
248 for (i, &cd) in live.powerup_cooldowns.iter().enumerate() {
249 assert!(
250 cd > 0,
251 "powerup_cooldowns[{i}] must be seeded non-zero by Default, got {cd}"
252 );
253 }
254 }
255}