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