Skip to main content

cuqueclicker_lib/save/versions/
v1.rs

1//! Save schema V1 — the pre-versioned shape that shipped on `main`.
2//!
3//! FROZEN. Do not edit this file except to fix a migration bug. New schema
4//! changes go in `v2.rs` together with a `From<GameStateV1> for GameStateV2`
5//! conversion.
6//!
7//! V1 is structurally identical to today's live `GameState`, but kept as a
8//! standalone copy so future divergence (V2 introduces per-fingerer
9//! modifiers, V3 inevitably something else) doesn't have to read the
10//! current code to know what V1 looked like.
11
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14
15use crate::game::state::GameState;
16
17/// V1 mirror of `Buff`. Frozen at the shape it had when V1 shipped:
18/// click-frenzy and per-fingerer-boost variants only. The `FingererBoost`
19/// variant is consumed by the V1→V2 chain step (`super::v2`); the V1
20/// snapshot itself stays unchanged.
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub enum BuffV1 {
23    ClickFrenzy {
24        ticks_remaining: u32,
25        initial_ticks: u32,
26        mult: f64,
27    },
28    FingererBoost {
29        ticks_remaining: u32,
30        initial_ticks: u32,
31        fingerer_id: String,
32        mult: f64,
33    },
34}
35
36/// V1 game-state snapshot. Holds only the persisted fields — everything
37/// `#[serde(skip)]` on the live `GameState` is ephemeral and gets seeded
38/// by `migrate_runtime()` after the chain finishes.
39#[derive(Clone, Serialize, Deserialize)]
40pub struct GameStateV1 {
41    #[serde(default)]
42    pub cuques: f64,
43    #[serde(default)]
44    pub total_clicks: u64,
45    #[serde(default)]
46    pub lifetime_cuques: f64,
47    #[serde(default)]
48    pub best_fps: f64,
49    #[serde(default)]
50    pub golden_caught: u64,
51    #[serde(default)]
52    pub fingerers_owned: HashMap<String, u32>,
53    #[serde(default)]
54    pub achievements_earned: HashSet<String>,
55    #[serde(default)]
56    pub upgrades_earned: HashSet<String>,
57    #[serde(default)]
58    pub prestige: u64,
59    #[serde(default)]
60    pub total_play_ticks: u64,
61    #[serde(default)]
62    pub buffs: Vec<BuffV1>,
63}
64
65impl GameStateV1 {
66    /// Convenience shortcut for tests and tooling: walk this V1 snapshot
67    /// through the migration chain to a live `GameState`. Folds through
68    /// every intermediate version. Production code goes through
69    /// `crate::save::load_from_str` instead, which also runs
70    /// `migrate_runtime()`.
71    pub fn into_current(self) -> GameState {
72        super::v2::GameStateV2::from(self).into_current()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::game::state::Buff;
80
81    #[test]
82    fn into_current_preserves_all_persisted_fields() {
83        let v1 = GameStateV1 {
84            cuques: 12345.6,
85            total_clicks: 42,
86            lifetime_cuques: 99999.0,
87            best_fps: 17.5,
88            golden_caught: 3,
89            fingerers_owned: [("index_finger".into(), 9), ("latex_glove".into(), 4)]
90                .into_iter()
91                .collect(),
92            achievements_earned: ["first_finger".into()].into_iter().collect(),
93            upgrades_earned: ["click_mult_1".into()].into_iter().collect(),
94            prestige: 7,
95            total_play_ticks: 1000,
96            buffs: vec![BuffV1::ClickFrenzy {
97                ticks_remaining: 100,
98                initial_ticks: 260,
99                mult: 777.0,
100            }],
101        };
102
103        let s = v1.into_current();
104
105        assert_eq!(s.version, crate::save::CURRENT_VERSION);
106        assert_eq!(s.cuques, 12345.6);
107        assert_eq!(s.total_clicks, 42);
108        assert_eq!(s.lifetime_cuques, 99999.0);
109        assert_eq!(s.best_fps, 17.5);
110        assert_eq!(s.golden_caught, 3);
111        assert_eq!(s.fingerer_count("index_finger"), 9);
112        assert_eq!(s.fingerer_count("latex_glove"), 4);
113        assert!(s.has_achievement("first_finger"));
114        assert!(s.has_upgrade("click_mult_1"));
115        assert_eq!(s.prestige, 7);
116        assert_eq!(s.total_play_ticks, 1000);
117        assert_eq!(s.buffs.len(), 1);
118        assert!(matches!(
119            s.buffs[0],
120            Buff::ClickFrenzy {
121                ticks_remaining: 100,
122                ..
123            }
124        ));
125    }
126
127    #[test]
128    fn in_flight_fingerer_boost_becomes_purple_modifier() {
129        // Active per-fingerer Buff in a V1 save reaches live state as a
130        // PurpleCoin modifier on the targeted fingerer with remaining time
131        // preserved. The legacy `Buff::FingererBoost` no longer exists.
132        let v1 = GameStateV1 {
133            buffs: vec![BuffV1::FingererBoost {
134                ticks_remaining: 600,
135                initial_ticks: 1200,
136                fingerer_id: "latex_glove".into(),
137                mult: 7.0,
138            }],
139            ..GameStateV1 {
140                cuques: 0.0,
141                total_clicks: 0,
142                lifetime_cuques: 0.0,
143                best_fps: 0.0,
144                golden_caught: 0,
145                fingerers_owned: HashMap::new(),
146                achievements_earned: HashSet::new(),
147                upgrades_earned: HashSet::new(),
148                prestige: 0,
149                total_play_ticks: 0,
150                buffs: vec![],
151            }
152        };
153
154        let s = v1.into_current();
155
156        // Live `Buff` enum is now click-only — FingererBoost is gone.
157        assert!(
158            s.buffs
159                .iter()
160                .all(|b| matches!(b, Buff::ClickFrenzy { .. }))
161        );
162
163        let st = s
164            .fingerers_state
165            .get("latex_glove")
166            .expect("modifier attached to target fingerer");
167        assert_eq!(st.modifiers.len(), 1);
168        let m = &st.modifiers[0];
169        assert!(matches!(
170            m.source,
171            crate::game::modifier::ModifierSource::PurpleCoin
172        ));
173        assert!(matches!(
174            m.duration,
175            crate::game::modifier::ModifierDuration::Ticks(600)
176        ));
177        // Remaining time and mult survive.
178        assert!((st.aggregate.mul_factor - 7.0).abs() < 1e-9);
179    }
180}