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::tree::UpgradeTreeState;
24
25fn default_v4_version() -> u32 {
26 4
27}
28
29#[derive(Clone, Serialize, Deserialize)]
30pub struct GameStateV4 {
31 #[serde(default = "default_v4_version")]
32 pub version: u32,
33 #[serde(default)]
34 pub cuques: f64,
35 #[serde(default)]
36 pub total_clicks: u64,
37 #[serde(default)]
38 pub lifetime_cuques: f64,
39 #[serde(default)]
40 pub best_fps: f64,
41 #[serde(default)]
42 pub golden_caught: u64,
43 #[serde(default)]
44 pub lucky_caught: u64,
45 #[serde(default)]
46 pub frenzy_caught: u64,
47 #[serde(default)]
48 pub buff_caught: u64,
49 #[serde(default)]
50 pub green_coin_caught: u64,
51 #[serde(default)]
52 pub fingerers_state: HashMap<String, FingererStateV2>,
53 #[serde(default)]
54 pub achievements_earned: HashSet<String>,
55 #[serde(default)]
56 pub prestige: u64,
57 #[serde(default)]
58 pub total_play_ticks: u64,
59 #[serde(default)]
60 pub buffs: Vec<BuffV2>,
61 #[serde(default)]
64 pub tree: UpgradeTreeState,
65}
66
67impl From<GameStateV3> for GameStateV4 {
83 fn from(v3: GameStateV3) -> Self {
84 GameStateV4 {
85 version: 4,
86 cuques: v3.cuques,
87 total_clicks: v3.total_clicks,
88 lifetime_cuques: v3.lifetime_cuques,
89 best_fps: v3.best_fps,
90 golden_caught: v3.golden_caught,
91 lucky_caught: v3.lucky_caught,
92 frenzy_caught: v3.frenzy_caught,
93 buff_caught: v3.buff_caught,
94 green_coin_caught: v3.green_coin_caught,
95 fingerers_state: v3.fingerers_state,
96 achievements_earned: v3.achievements_earned,
97 prestige: v3.prestige,
99 total_play_ticks: v3.total_play_ticks,
100 buffs: v3.buffs,
101 tree: UpgradeTreeState::default(),
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::game::tree::coord::TreeCoord;
110 use crate::save::versions::v2::{
111 FingererStateV2, ModifierDurationV2, ModifierEffectV2, ModifierSourceV2, ModifierV2,
112 };
113
114 fn empty_v3() -> GameStateV3 {
115 GameStateV3 {
116 version: 3,
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 }
133 }
134
135 #[test]
136 fn v3_to_v4_drops_upgrades_earned_silently() {
137 let mut v3 = empty_v3();
138 v3.upgrades_earned.insert("click_mult_1".into());
139 v3.upgrades_earned.insert("hand_of_god_mult_3".into());
140 let v4: GameStateV4 = v3.into();
141 assert_eq!(v4.version, 4);
142 let json = serde_json::to_string(&v4).expect("serialize");
145 assert!(
146 !json.contains("upgrades_earned"),
147 "old upgrades field must not appear in V4 JSON: {json}"
148 );
149 }
150
151 #[test]
152 fn v3_to_v4_inits_empty_tree() {
153 let v4: GameStateV4 = empty_v3().into();
154 assert!(v4.tree.bought.is_empty());
155 assert_eq!(v4.tree.cursor, TreeCoord::ORIGIN);
156 assert_eq!(v4.tree.last_bought, None);
157 }
158
159 #[test]
160 fn v3_to_v4_preserves_all_other_fields() {
161 let v3 = GameStateV3 {
162 version: 3,
163 cuques: 1234.5,
164 total_clicks: 99,
165 lifetime_cuques: 5_000.0,
166 best_fps: 12.0,
167 golden_caught: 17,
168 lucky_caught: 10,
169 frenzy_caught: 4,
170 buff_caught: 2,
171 green_coin_caught: 1,
172 fingerers_state: [(
173 "index_finger".to_string(),
174 FingererStateV2 {
175 count: 9,
176 modifiers: vec![ModifierV2 {
177 source: ModifierSourceV2::GreenCoin,
178 effects: vec![ModifierEffectV2::AddPercent(0.10)],
179 duration: ModifierDurationV2::Permanent,
180 created_at_tick: 0,
181 }],
182 },
183 )]
184 .into_iter()
185 .collect(),
186 achievements_earned: ["first_finger".into()].into_iter().collect(),
187 upgrades_earned: ["click_mult_1".into()].into_iter().collect(),
188 prestige: 3,
189 total_play_ticks: 9_000,
190 buffs: vec![BuffV2::ClickFrenzy {
191 ticks_remaining: 100,
192 initial_ticks: 260,
193 mult: 777.0,
194 }],
195 };
196
197 let v4: GameStateV4 = v3.into();
198
199 assert_eq!(v4.cuques, 1234.5);
200 assert_eq!(v4.total_clicks, 99);
201 assert_eq!(v4.lifetime_cuques, 5_000.0);
202 assert_eq!(v4.best_fps, 12.0);
203 assert_eq!(v4.golden_caught, 17);
204 assert_eq!(v4.lucky_caught, 10);
205 assert_eq!(v4.frenzy_caught, 4);
206 assert_eq!(v4.buff_caught, 2);
207 assert_eq!(v4.green_coin_caught, 1);
208 assert_eq!(v4.prestige, 3);
209 assert_eq!(v4.total_play_ticks, 9_000);
210 assert_eq!(v4.buffs.len(), 1);
211 let st = v4.fingerers_state.get("index_finger").unwrap();
212 assert_eq!(st.count, 9);
213 assert_eq!(st.modifiers.len(), 1);
214 assert!(v4.achievements_earned.contains("first_finger"));
215 }
216
217 #[test]
218 fn v4_through_v5_preserves_tree_state() {
219 let mut v4 = GameStateV4 {
222 version: 4,
223 cuques: 0.0,
224 total_clicks: 0,
225 lifetime_cuques: 0.0,
226 best_fps: 0.0,
227 golden_caught: 0,
228 lucky_caught: 0,
229 frenzy_caught: 0,
230 buff_caught: 0,
231 green_coin_caught: 0,
232 fingerers_state: HashMap::new(),
233 achievements_earned: HashSet::new(),
234 prestige: 0,
235 total_play_ticks: 0,
236 buffs: vec![],
237 tree: UpgradeTreeState::default(),
238 };
239 v4.tree.bought.insert(TreeCoord::ORIGIN);
240
241 let live = crate::save::versions::v5::GameStateV5::from(v4).into_current();
242 assert!(live.tree.bought.contains(&TreeCoord::ORIGIN));
243 }
244}