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, crate::bignum::Mag::from_f64(12345.6));
107        assert_eq!(s.total_clicks, 42);
108        assert_eq!(s.lifetime_cuques, crate::bignum::Mag::from_f64(99999.0));
109        assert_eq!(s.best_fps, crate::bignum::Mag::from_f64(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        // V3→V4 (the 1.0.0 breaking change) silently drops the old
115        // hardcoded upgrades. A V1 save's `upgrades_earned` field is
116        // accepted by the V1 deserializer but the chain ends with an
117        // empty tree.
118        assert!(s.tree.bought.is_empty());
119        assert_eq!(s.prestige, 7);
120        assert_eq!(s.total_play_ticks, 1000);
121        assert_eq!(s.buffs.len(), 1);
122        assert!(matches!(
123            s.buffs[0],
124            Buff::ClickFrenzy {
125                ticks_remaining: 100,
126                ..
127            }
128        ));
129    }
130
131    #[test]
132    fn in_flight_fingerer_boost_becomes_purple_modifier() {
133        // Active per-fingerer Buff in a V1 save reaches live state as a
134        // PurpleCoin modifier on the targeted fingerer with remaining time
135        // preserved. The legacy `Buff::FingererBoost` no longer exists.
136        let v1 = GameStateV1 {
137            buffs: vec![BuffV1::FingererBoost {
138                ticks_remaining: 600,
139                initial_ticks: 1200,
140                fingerer_id: "latex_glove".into(),
141                mult: 7.0,
142            }],
143            ..GameStateV1 {
144                cuques: 0.0,
145                total_clicks: 0,
146                lifetime_cuques: 0.0,
147                best_fps: 0.0,
148                golden_caught: 0,
149                fingerers_owned: HashMap::new(),
150                achievements_earned: HashSet::new(),
151                upgrades_earned: HashSet::new(),
152                prestige: 0,
153                total_play_ticks: 0,
154                buffs: vec![],
155            }
156        };
157
158        let s = v1.into_current();
159
160        // Live `Buff` enum is now click-only — FingererBoost is gone.
161        assert!(
162            s.buffs
163                .iter()
164                .all(|b| matches!(b, Buff::ClickFrenzy { .. }))
165        );
166
167        let st = s
168            .fingerers_state
169            .get("latex_glove")
170            .expect("modifier attached to target fingerer");
171        assert_eq!(st.modifiers.len(), 1);
172        let m = &st.modifiers[0];
173        assert!(matches!(
174            m.source,
175            crate::game::modifier::ModifierSource::PurpleCoin
176        ));
177        assert!(matches!(
178            m.duration,
179            crate::game::modifier::ModifierDuration::Ticks(600)
180        ));
181        // Remaining time and mult survive.
182        assert!((st.aggregate.mul_factor.to_f64() - 7.0).abs() < 1e-9);
183    }
184}