cuqueclicker_lib/save/versions/
v4.rs1use serde::{Deserialize, Serialize};
19use std::collections::{HashMap, HashSet};
20
21use super::v2::{BuffV2, FingererStateV2};
22use super::v3::GameStateV3;
23use crate::game::state::GameState;
24use crate::game::tree::UpgradeTreeState;
25
26fn default_v4_version() -> u32 {
27 4
28}
29
30#[derive(Clone, Serialize, Deserialize)]
31pub struct GameStateV4 {
32 #[serde(default = "default_v4_version")]
33 pub version: u32,
34 #[serde(default)]
35 pub cuques: f64,
36 #[serde(default)]
37 pub total_clicks: u64,
38 #[serde(default)]
39 pub lifetime_cuques: f64,
40 #[serde(default)]
41 pub best_fps: f64,
42 #[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 prestige: u64,
58 #[serde(default)]
59 pub total_play_ticks: u64,
60 #[serde(default)]
61 pub buffs: Vec<BuffV2>,
62 #[serde(default)]
65 pub tree: UpgradeTreeState,
66}
67
68impl GameStateV4 {
69 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 prestige: self.prestige,
93 total_play_ticks: self.total_play_ticks,
94 buffs,
95 tree: self.tree,
96 ..GameState::default()
97 }
98 }
99}
100
101impl From<GameStateV3> for GameStateV4 {
110 fn from(v3: GameStateV3) -> Self {
111 GameStateV4 {
112 version: 4,
113 cuques: v3.cuques,
114 total_clicks: v3.total_clicks,
115 lifetime_cuques: v3.lifetime_cuques,
116 best_fps: v3.best_fps,
117 golden_caught: v3.golden_caught,
118 lucky_caught: v3.lucky_caught,
119 frenzy_caught: v3.frenzy_caught,
120 buff_caught: v3.buff_caught,
121 green_coin_caught: v3.green_coin_caught,
122 fingerers_state: v3.fingerers_state,
123 achievements_earned: v3.achievements_earned,
124 prestige: v3.prestige,
126 total_play_ticks: v3.total_play_ticks,
127 buffs: v3.buffs,
128 tree: UpgradeTreeState::default(),
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::game::tree::coord::TreeCoord;
137 use crate::save::versions::v2::{
138 FingererStateV2, ModifierDurationV2, ModifierEffectV2, ModifierSourceV2, ModifierV2,
139 };
140
141 fn empty_v3() -> GameStateV3 {
142 GameStateV3 {
143 version: 3,
144 cuques: 0.0,
145 total_clicks: 0,
146 lifetime_cuques: 0.0,
147 best_fps: 0.0,
148 golden_caught: 0,
149 lucky_caught: 0,
150 frenzy_caught: 0,
151 buff_caught: 0,
152 green_coin_caught: 0,
153 fingerers_state: HashMap::new(),
154 achievements_earned: HashSet::new(),
155 upgrades_earned: HashSet::new(),
156 prestige: 0,
157 total_play_ticks: 0,
158 buffs: vec![],
159 }
160 }
161
162 #[test]
163 fn v3_to_v4_drops_upgrades_earned_silently() {
164 let mut v3 = empty_v3();
165 v3.upgrades_earned.insert("click_mult_1".into());
166 v3.upgrades_earned.insert("hand_of_god_mult_3".into());
167 let v4: GameStateV4 = v3.into();
168 assert_eq!(v4.version, 4);
169 let json = serde_json::to_string(&v4).expect("serialize");
172 assert!(
173 !json.contains("upgrades_earned"),
174 "old upgrades field must not appear in V4 JSON: {json}"
175 );
176 }
177
178 #[test]
179 fn v3_to_v4_inits_empty_tree() {
180 let v4: GameStateV4 = empty_v3().into();
181 assert!(v4.tree.bought.is_empty());
182 assert_eq!(v4.tree.cursor, TreeCoord::ORIGIN);
183 assert_eq!(v4.tree.last_bought, None);
184 }
185
186 #[test]
187 fn v3_to_v4_preserves_all_other_fields() {
188 let v3 = GameStateV3 {
189 version: 3,
190 cuques: 1234.5,
191 total_clicks: 99,
192 lifetime_cuques: 5_000.0,
193 best_fps: 12.0,
194 golden_caught: 17,
195 lucky_caught: 10,
196 frenzy_caught: 4,
197 buff_caught: 2,
198 green_coin_caught: 1,
199 fingerers_state: [(
200 "index_finger".to_string(),
201 FingererStateV2 {
202 count: 9,
203 modifiers: vec![ModifierV2 {
204 source: ModifierSourceV2::GreenCoin,
205 effects: vec![ModifierEffectV2::AddPercent(0.10)],
206 duration: ModifierDurationV2::Permanent,
207 created_at_tick: 0,
208 }],
209 },
210 )]
211 .into_iter()
212 .collect(),
213 achievements_earned: ["first_finger".into()].into_iter().collect(),
214 upgrades_earned: ["click_mult_1".into()].into_iter().collect(),
215 prestige: 3,
216 total_play_ticks: 9_000,
217 buffs: vec![BuffV2::ClickFrenzy {
218 ticks_remaining: 100,
219 initial_ticks: 260,
220 mult: 777.0,
221 }],
222 };
223
224 let v4: GameStateV4 = v3.into();
225
226 assert_eq!(v4.cuques, 1234.5);
227 assert_eq!(v4.total_clicks, 99);
228 assert_eq!(v4.lifetime_cuques, 5_000.0);
229 assert_eq!(v4.best_fps, 12.0);
230 assert_eq!(v4.golden_caught, 17);
231 assert_eq!(v4.lucky_caught, 10);
232 assert_eq!(v4.frenzy_caught, 4);
233 assert_eq!(v4.buff_caught, 2);
234 assert_eq!(v4.green_coin_caught, 1);
235 assert_eq!(v4.prestige, 3);
236 assert_eq!(v4.total_play_ticks, 9_000);
237 assert_eq!(v4.buffs.len(), 1);
238 let st = v4.fingerers_state.get("index_finger").unwrap();
239 assert_eq!(st.count, 9);
240 assert_eq!(st.modifiers.len(), 1);
241 assert!(v4.achievements_earned.contains("first_finger"));
242 }
243
244 #[test]
245 fn v4_into_current_rebuilds_tree_aggregate() {
246 let mut v4 = GameStateV4 {
251 version: 4,
252 cuques: 0.0,
253 total_clicks: 0,
254 lifetime_cuques: 0.0,
255 best_fps: 0.0,
256 golden_caught: 0,
257 lucky_caught: 0,
258 frenzy_caught: 0,
259 buff_caught: 0,
260 green_coin_caught: 0,
261 fingerers_state: HashMap::new(),
262 achievements_earned: HashSet::new(),
263 prestige: 0,
264 total_play_ticks: 0,
265 buffs: vec![],
266 tree: UpgradeTreeState::default(),
267 };
268 v4.tree.bought.insert(TreeCoord::ORIGIN);
269
270 let live = v4.into_current();
271 assert!(live.tree.bought.contains(&TreeCoord::ORIGIN));
272 }
273}