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