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