1use serde::{Deserialize, Serialize};
14use std::collections::{HashMap, HashSet};
15
16use super::v1::{BuffV1, GameStateV1};
17use crate::game::modifier::{
18 FingererAggregate, Modifier, ModifierDuration, ModifierEffect, ModifierSource,
19};
20use crate::game::state::{Buff, FingererState, GameState};
21
22#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
23pub enum ModifierSourceV2 {
24 GreenCoin,
25 PurpleCoin,
26}
27
28impl From<ModifierSourceV2> for ModifierSource {
29 fn from(s: ModifierSourceV2) -> Self {
30 match s {
31 ModifierSourceV2::GreenCoin => ModifierSource::GreenCoin,
32 ModifierSourceV2::PurpleCoin => ModifierSource::PurpleCoin,
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
38pub enum ModifierEffectV2 {
39 FlatFps(f64),
40 AddPercent(f64),
41 MulFactor(f64),
42}
43
44impl From<ModifierEffectV2> for ModifierEffect {
45 fn from(e: ModifierEffectV2) -> Self {
46 match e {
47 ModifierEffectV2::FlatFps(v) => ModifierEffect::FlatFps(v),
48 ModifierEffectV2::AddPercent(v) => ModifierEffect::AddPercent(v),
49 ModifierEffectV2::MulFactor(v) => ModifierEffect::MulFactor(v),
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
55pub enum ModifierDurationV2 {
56 Permanent,
57 Ticks(u32),
58}
59
60impl From<ModifierDurationV2> for ModifierDuration {
61 fn from(d: ModifierDurationV2) -> Self {
62 match d {
63 ModifierDurationV2::Permanent => ModifierDuration::Permanent,
64 ModifierDurationV2::Ticks(n) => ModifierDuration::Ticks(n),
65 }
66 }
67}
68
69#[derive(Clone, Debug, Serialize, Deserialize)]
70pub struct ModifierV2 {
71 pub source: ModifierSourceV2,
72 pub effects: Vec<ModifierEffectV2>,
73 pub duration: ModifierDurationV2,
74 #[serde(default)]
75 pub created_at_tick: u64,
76}
77
78impl From<ModifierV2> for Modifier {
79 fn from(m: ModifierV2) -> Self {
80 Modifier {
81 source: m.source.into(),
82 effects: m.effects.into_iter().map(Into::into).collect(),
83 duration: m.duration.into(),
84 created_at_tick: m.created_at_tick,
85 }
86 }
87}
88
89#[derive(Clone, Debug, Default, Serialize, Deserialize)]
90pub struct FingererStateV2 {
91 #[serde(default)]
92 pub count: u32,
93 #[serde(default)]
94 pub modifiers: Vec<ModifierV2>,
95}
96
97impl From<FingererStateV2> for FingererState {
98 fn from(v: FingererStateV2) -> Self {
99 let modifiers: Vec<Modifier> = v.modifiers.into_iter().map(Into::into).collect();
100 let aggregate = FingererAggregate::rebuild(&modifiers);
101 FingererState {
102 count: v.count,
103 modifiers,
104 aggregate,
105 }
106 }
107}
108
109#[derive(Clone, Debug, Serialize, Deserialize)]
115pub enum BuffV2 {
116 ClickFrenzy {
117 ticks_remaining: u32,
118 initial_ticks: u32,
119 mult: f64,
120 },
121}
122
123impl From<BuffV2> for Buff {
124 fn from(b: BuffV2) -> Self {
125 match b {
126 BuffV2::ClickFrenzy {
127 ticks_remaining,
128 initial_ticks,
129 mult,
130 } => Buff::ClickFrenzy {
131 ticks_remaining,
132 initial_ticks,
133 mult,
134 },
135 }
136 }
137}
138
139fn default_v2_version() -> u32 {
140 2
141}
142
143#[derive(Clone, Serialize, Deserialize)]
144pub struct GameStateV2 {
145 #[serde(default = "default_v2_version")]
146 pub version: u32,
147 #[serde(default)]
148 pub cuques: f64,
149 #[serde(default)]
150 pub total_clicks: u64,
151 #[serde(default)]
152 pub lifetime_cuques: f64,
153 #[serde(default)]
154 pub best_fps: f64,
155 #[serde(default)]
160 pub golden_caught: u64,
161 #[serde(default)]
166 pub lucky_caught: u64,
167 #[serde(default)]
168 pub frenzy_caught: u64,
169 #[serde(default)]
170 pub buff_caught: u64,
171 #[serde(default)]
172 pub green_coin_caught: u64,
173 #[serde(default)]
174 pub fingerers_state: HashMap<String, FingererStateV2>,
175 #[serde(default)]
176 pub achievements_earned: HashSet<String>,
177 #[serde(default)]
178 pub upgrades_earned: HashSet<String>,
179 #[serde(default)]
180 pub prestige: u64,
181 #[serde(default)]
182 pub total_play_ticks: u64,
183 #[serde(default)]
184 pub buffs: Vec<BuffV2>,
185 #[serde(default)]
189 pub goldens_since_green_coin: u32,
190}
191
192impl GameStateV2 {
193 pub fn into_current(self) -> GameState {
198 let fingerers_state = self
199 .fingerers_state
200 .into_iter()
201 .map(|(id, st)| (id, st.into()))
202 .collect();
203 let buffs = self.buffs.into_iter().map(Into::into).collect();
204 GameState {
205 version: crate::save::CURRENT_VERSION,
206 cuques: self.cuques,
207 total_clicks: self.total_clicks,
208 lifetime_cuques: self.lifetime_cuques,
209 best_fps: self.best_fps,
210 golden_caught: self.golden_caught,
211 lucky_caught: self.lucky_caught,
212 frenzy_caught: self.frenzy_caught,
213 buff_caught: self.buff_caught,
214 green_coin_caught: self.green_coin_caught,
215 fingerers_state,
216 achievements_earned: self.achievements_earned,
217 upgrades_earned: self.upgrades_earned,
218 prestige: self.prestige,
219 total_play_ticks: self.total_play_ticks,
220 buffs,
221 goldens_since_green_coin: self.goldens_since_green_coin,
222 ..GameState::default()
223 }
224 }
225}
226
227impl From<GameStateV1> for GameStateV2 {
238 fn from(v1: GameStateV1) -> Self {
239 let mut fingerers_state: HashMap<String, FingererStateV2> = v1
240 .fingerers_owned
241 .into_iter()
242 .map(|(id, count)| {
243 (
244 id,
245 FingererStateV2 {
246 count,
247 modifiers: vec![],
248 },
249 )
250 })
251 .collect();
252 let mut buffs: Vec<BuffV2> = Vec::new();
253 for b in v1.buffs {
254 match b {
255 BuffV1::ClickFrenzy {
256 ticks_remaining,
257 initial_ticks,
258 mult,
259 } => buffs.push(BuffV2::ClickFrenzy {
260 ticks_remaining,
261 initial_ticks,
262 mult,
263 }),
264 BuffV1::FingererBoost {
265 ticks_remaining,
266 initial_ticks,
267 fingerer_id,
268 mult,
269 } => {
270 let elapsed = initial_ticks.saturating_sub(ticks_remaining) as u64;
276 let created_at_tick = v1.total_play_ticks.saturating_sub(elapsed);
277 let st = fingerers_state.entry(fingerer_id).or_default();
278 st.modifiers.push(ModifierV2 {
279 source: ModifierSourceV2::PurpleCoin,
280 effects: vec![ModifierEffectV2::MulFactor(mult)],
281 duration: ModifierDurationV2::Ticks(ticks_remaining),
282 created_at_tick,
283 });
284 }
285 }
286 }
287 GameStateV2 {
288 version: 2,
289 cuques: v1.cuques,
290 total_clicks: v1.total_clicks,
291 lifetime_cuques: v1.lifetime_cuques,
292 best_fps: v1.best_fps,
293 golden_caught: v1.golden_caught,
294 lucky_caught: 0,
297 frenzy_caught: 0,
298 buff_caught: 0,
299 green_coin_caught: 0,
300 fingerers_state,
301 achievements_earned: v1.achievements_earned,
302 upgrades_earned: v1.upgrades_earned,
303 prestige: v1.prestige,
304 total_play_ticks: v1.total_play_ticks,
305 buffs,
306 goldens_since_green_coin: 0,
309 }
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn v1_to_v2_preserves_fingerer_counts() {
319 let v1 = GameStateV1 {
320 cuques: 0.0,
321 total_clicks: 0,
322 lifetime_cuques: 0.0,
323 best_fps: 0.0,
324 golden_caught: 0,
325 fingerers_owned: [("index_finger".into(), 9), ("latex_glove".into(), 4)]
326 .into_iter()
327 .collect(),
328 achievements_earned: HashSet::new(),
329 upgrades_earned: HashSet::new(),
330 prestige: 0,
331 total_play_ticks: 0,
332 buffs: vec![],
333 };
334
335 let v2: GameStateV2 = v1.into();
336
337 assert_eq!(v2.version, 2);
338 assert_eq!(v2.fingerers_state.get("index_finger").unwrap().count, 9);
339 assert_eq!(v2.fingerers_state.get("latex_glove").unwrap().count, 4);
340 assert!(
341 v2.fingerers_state
342 .values()
343 .all(|st| st.modifiers.is_empty())
344 );
345 }
346
347 #[test]
348 fn v1_to_v2_absorbs_in_flight_fingerer_boost_into_modifier() {
349 let v1 = GameStateV1 {
355 cuques: 0.0,
356 total_clicks: 0,
357 lifetime_cuques: 0.0,
358 best_fps: 0.0,
359 golden_caught: 0,
360 fingerers_owned: [("latex_glove".into(), 4)].into_iter().collect(),
361 achievements_earned: HashSet::new(),
362 upgrades_earned: HashSet::new(),
363 prestige: 0,
364 total_play_ticks: 1500,
365 buffs: vec![BuffV1::FingererBoost {
366 ticks_remaining: 600,
367 initial_ticks: 1200,
368 fingerer_id: "latex_glove".into(),
369 mult: 7.0,
370 }],
371 };
372
373 let v2: GameStateV2 = v1.into();
374
375 assert!(v2.buffs.is_empty());
377
378 let st = v2
379 .fingerers_state
380 .get("latex_glove")
381 .expect("fingerer entry preserved");
382 assert_eq!(st.count, 4);
383 assert_eq!(st.modifiers.len(), 1);
384 let m = &st.modifiers[0];
385 assert!(matches!(m.source, ModifierSourceV2::PurpleCoin));
386 assert!(matches!(m.duration, ModifierDurationV2::Ticks(600)));
387 assert!(matches!(
388 m.effects[0],
389 ModifierEffectV2::MulFactor(v) if (v - 7.0).abs() < 1e-9
390 ));
391 assert_eq!(m.created_at_tick, 900);
393 }
394
395 #[test]
396 fn v1_to_v2_absorbed_modifier_attaches_to_unowned_fingerer_safely() {
397 let v1 = GameStateV1 {
403 cuques: 0.0,
404 total_clicks: 0,
405 lifetime_cuques: 0.0,
406 best_fps: 0.0,
407 golden_caught: 0,
408 fingerers_owned: HashMap::new(),
409 achievements_earned: HashSet::new(),
410 upgrades_earned: HashSet::new(),
411 prestige: 0,
412 total_play_ticks: 0,
413 buffs: vec![BuffV1::FingererBoost {
414 ticks_remaining: 100,
415 initial_ticks: 100,
416 fingerer_id: "hand_of_god".into(),
417 mult: 7.0,
418 }],
419 };
420
421 let v2: GameStateV2 = v1.into();
422
423 let st = v2
424 .fingerers_state
425 .get("hand_of_god")
426 .expect("entry created");
427 assert_eq!(st.count, 0);
428 assert_eq!(st.modifiers.len(), 1);
429 }
430
431 #[test]
432 fn v1_to_v2_passes_through_click_frenzy() {
433 let v1 = GameStateV1 {
434 cuques: 0.0,
435 total_clicks: 0,
436 lifetime_cuques: 0.0,
437 best_fps: 0.0,
438 golden_caught: 0,
439 fingerers_owned: HashMap::new(),
440 achievements_earned: HashSet::new(),
441 upgrades_earned: HashSet::new(),
442 prestige: 0,
443 total_play_ticks: 0,
444 buffs: vec![BuffV1::ClickFrenzy {
445 ticks_remaining: 100,
446 initial_ticks: 260,
447 mult: 777.0,
448 }],
449 };
450
451 let v2: GameStateV2 = v1.into();
452
453 assert_eq!(v2.buffs.len(), 1);
454 assert!(matches!(
455 v2.buffs[0],
456 BuffV2::ClickFrenzy {
457 ticks_remaining: 100,
458 ..
459 }
460 ));
461 }
462
463 #[test]
464 fn v2_into_current_rebuilds_aggregate_from_modifiers() {
465 let v2 = GameStateV2 {
469 version: 2,
470 cuques: 0.0,
471 total_clicks: 0,
472 lifetime_cuques: 0.0,
473 best_fps: 0.0,
474 golden_caught: 0,
475 lucky_caught: 0,
476 frenzy_caught: 0,
477 buff_caught: 0,
478 green_coin_caught: 0,
479 fingerers_state: [(
480 "latex_glove".to_string(),
481 FingererStateV2 {
482 count: 5,
483 modifiers: vec![
484 ModifierV2 {
485 source: ModifierSourceV2::GreenCoin,
486 effects: vec![ModifierEffectV2::AddPercent(0.10)],
487 duration: ModifierDurationV2::Permanent,
488 created_at_tick: 0,
489 },
490 ModifierV2 {
491 source: ModifierSourceV2::PurpleCoin,
492 effects: vec![ModifierEffectV2::MulFactor(2.0)],
493 duration: ModifierDurationV2::Ticks(600),
494 created_at_tick: 0,
495 },
496 ],
497 },
498 )]
499 .into_iter()
500 .collect(),
501 achievements_earned: HashSet::new(),
502 upgrades_earned: HashSet::new(),
503 prestige: 0,
504 total_play_ticks: 0,
505 buffs: vec![],
506 goldens_since_green_coin: 0,
507 };
508
509 let live = v2.into_current();
510 let st = live.fingerers_state.get("latex_glove").unwrap();
511 assert_eq!(st.count, 5);
512 assert_eq!(st.modifiers.len(), 2);
513 assert!((st.aggregate.add_percent - 0.10).abs() < 1e-9);
514 assert!((st.aggregate.mul_factor - 2.0).abs() < 1e-9);
515 }
516
517 #[test]
518 fn v1_to_v2_zero_inits_per_variant_counters() {
519 let v1 = GameStateV1 {
524 cuques: 0.0,
525 total_clicks: 0,
526 lifetime_cuques: 0.0,
527 best_fps: 0.0,
528 golden_caught: 17,
529 fingerers_owned: HashMap::new(),
530 achievements_earned: HashSet::new(),
531 upgrades_earned: HashSet::new(),
532 prestige: 0,
533 total_play_ticks: 0,
534 buffs: vec![],
535 };
536
537 let v2: GameStateV2 = v1.into();
538
539 assert_eq!(v2.golden_caught, 17, "rollup carried forward");
540 assert_eq!(v2.lucky_caught, 0);
541 assert_eq!(v2.frenzy_caught, 0);
542 assert_eq!(v2.buff_caught, 0);
543 assert_eq!(v2.green_coin_caught, 0);
544 }
545
546 #[test]
547 fn v2_into_current_preserves_per_variant_counters() {
548 let v2 = GameStateV2 {
549 version: 2,
550 cuques: 0.0,
551 total_clicks: 0,
552 lifetime_cuques: 0.0,
553 best_fps: 0.0,
554 golden_caught: 100,
555 lucky_caught: 60,
556 frenzy_caught: 20,
557 buff_caught: 15,
558 green_coin_caught: 5,
559 fingerers_state: HashMap::new(),
560 achievements_earned: HashSet::new(),
561 upgrades_earned: HashSet::new(),
562 prestige: 0,
563 total_play_ticks: 0,
564 buffs: vec![],
565 goldens_since_green_coin: 0,
566 };
567
568 let live = v2.into_current();
569
570 assert_eq!(live.golden_caught, 100);
571 assert_eq!(live.lucky_caught, 60);
572 assert_eq!(live.frenzy_caught, 20);
573 assert_eq!(live.buff_caught, 15);
574 assert_eq!(live.green_coin_caught, 5);
575 }
576}