Skip to main content

cuqueclicker_lib/game/tree/
primitive.rs

1//! Engine primitives — the fixed vocabulary of effects the tree can produce.
2//!
3//! Each tree node is a `Vec<Primitive>` where each primitive is a triple of
4//! `(Op, Target, magnitude)`. Op + Target are code-defined enums (the engine
5//! needs to dispatch on them); the **composition, count, target, magnitude,
6//! and sign** of each primitive at a given lot are 100% procgen.
7//!
8//! Adding a new primitive variant: append to `Op` or `Target`, add a fold
9//! case in `aggregate.rs::TreeAggregate::fold_in`, and add a render case in
10//! `naming.rs`. There's no other table to update.
11
12use crate::game::fingerer::FINGERERS;
13use crate::game::powerup::PowerupKind;
14
15/// What math the primitive performs. Sign of `magnitude` decides boon vs
16/// bane: a `MulFactor` with magnitude `< 1.0` is a debuff, `> 1.0` a buff.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub enum Op {
19    /// Add to the additive percent on the target (target's `add_percent`
20    /// gains `magnitude`). `0.10` = +10%.
21    AddPercent,
22    /// Multiply the multiplicative factor on the target. `2.0` = ×2,
23    /// `0.5` = ÷2.
24    MulFactor,
25    /// Add to the flat FPS contribution on the target.
26    FlatAdd,
27    /// Multiply the COST of buying a fingerer (`< 1.0` is a discount,
28    /// `> 1.0` is inflation). Only meaningful when target is a fingerer.
29    CostMul,
30    /// Scale the inter-arrival cooldown of a powerup kind. `< 1.0` =
31    /// spawns more often, `> 1.0` = rarer. Only meaningful when target is
32    /// a PowerupSpawn(kind).
33    SpawnRateMul,
34    /// Scale the reward / duration of a powerup-related effect. Used with
35    /// `Target::PowerupReward(kind)` and `Target::PowerupDuration(kind)`.
36    EffectMul,
37}
38
39/// What the primitive operates on. The full set of game-touchable axes —
40/// any effect we want the tree to express has to map to one of these.
41#[derive(Clone, Copy, Debug, PartialEq)]
42pub enum Target {
43    /// A specific fingerer by catalog index. `idx` is `0..FINGERERS.len()`.
44    /// Stable: `idx` is the position in the `FINGERERS` array. The catalog
45    /// id at that index is the load-bearing string; `idx` is just a
46    /// compact encoding for the procgen-derived target.
47    Fingerer(u8),
48    /// All fingerers — the effect distributes across every fingerer's
49    /// per-tier output.
50    AllFingerers,
51    /// Manual click power (`click_power()`).
52    Click,
53    /// Powerup spawn rate for the given kind (used with `Op::SpawnRateMul`).
54    PowerupSpawn(PowerupKind),
55    /// Powerup reward for the given kind (used with `Op::EffectMul`).
56    /// Currently meaningful for Lucky (flat cuques) and Buff (mul factor).
57    PowerupReward(PowerupKind),
58    /// Powerup buff duration for the given kind (used with `Op::EffectMul`).
59    /// Currently meaningful for Frenzy and Buff.
60    PowerupDuration(PowerupKind),
61    /// Prestige multiplier (current formula is 1 + 0.01 * prestige_count).
62    Prestige,
63    /// Green Coin AddPercent strength (the +10% becomes +10% * magnitude).
64    GreenCoinStrength,
65}
66
67impl Target {
68    /// Stable index used by the procgen to pick a target uniformly from the
69    /// available axes. Distinct values are returned for distinct targets.
70    pub fn from_index(idx: usize) -> Self {
71        let nf = FINGERERS.len();
72        // Layout:
73        //   [0..nf)            -> Fingerer(i)
74        //   [nf]               -> AllFingerers
75        //   [nf+1]             -> Click
76        //   [nf+2 ..nf+6)      -> PowerupSpawn(L/F/B/G)
77        //   [nf+6 ..nf+10)     -> PowerupReward(L/F/B/G)
78        //   [nf+10..nf+14)     -> PowerupDuration(L/F/B/G)
79        //   [nf+14]            -> Prestige
80        //   [nf+15]            -> GreenCoinStrength
81        let kinds = PowerupKind::ALL;
82        if idx < nf {
83            return Target::Fingerer(idx as u8);
84        }
85        let i = idx - nf;
86        if i == 0 {
87            return Target::AllFingerers;
88        }
89        if i == 1 {
90            return Target::Click;
91        }
92        if (2..6).contains(&i) {
93            return Target::PowerupSpawn(kinds[i - 2]);
94        }
95        if (6..10).contains(&i) {
96            return Target::PowerupReward(kinds[i - 6]);
97        }
98        if (10..14).contains(&i) {
99            return Target::PowerupDuration(kinds[i - 10]);
100        }
101        if i == 14 {
102            return Target::Prestige;
103        }
104        // Last bucket, anything beyond falls into GreenCoinStrength.
105        Target::GreenCoinStrength
106    }
107
108    /// Total number of distinct targets the procgen can produce, given the
109    /// current FINGERERS catalog length. Procgen uses
110    /// `rng.range_usize(target_count())` to pick.
111    pub fn target_count() -> usize {
112        FINGERERS.len() + 16
113    }
114}
115
116/// A single tree-node effect contribution.
117#[derive(Clone, Copy, Debug, PartialEq)]
118pub struct Primitive {
119    pub op: Op,
120    pub target: Target,
121    pub magnitude: f64,
122}
123
124impl Primitive {
125    /// True when the primitive's sign (relative to its op's "neutral" value)
126    /// represents a debuff/bane. Used to classify nodes as keystones.
127    pub fn is_bane(self) -> bool {
128        match self.op {
129            Op::AddPercent | Op::FlatAdd => self.magnitude < 0.0,
130            // For multiplicative ops a magnitude < 1.0 is a nerf (incl 0).
131            Op::MulFactor | Op::EffectMul | Op::SpawnRateMul => self.magnitude < 1.0,
132            // CostMul: < 1.0 is a *boon* (cheaper); > 1.0 is a bane.
133            Op::CostMul => self.magnitude > 1.0,
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn target_from_index_covers_full_range() {
144        let n = Target::target_count();
145        let mut seen_fingerer = 0;
146        let mut seen_all = 0;
147        let mut seen_click = 0;
148        let mut seen_prestige = 0;
149        for i in 0..n {
150            match Target::from_index(i) {
151                Target::Fingerer(_) => seen_fingerer += 1,
152                Target::AllFingerers => seen_all += 1,
153                Target::Click => seen_click += 1,
154                Target::Prestige => seen_prestige += 1,
155                _ => {}
156            }
157        }
158        assert_eq!(seen_fingerer, FINGERERS.len());
159        assert_eq!(seen_all, 1);
160        assert_eq!(seen_click, 1);
161        assert_eq!(seen_prestige, 1);
162    }
163
164    #[test]
165    fn is_bane_sign_semantics() {
166        // AddPercent: negative is bane.
167        assert!(
168            !Primitive {
169                op: Op::AddPercent,
170                target: Target::Click,
171                magnitude: 0.10
172            }
173            .is_bane()
174        );
175        assert!(
176            Primitive {
177                op: Op::AddPercent,
178                target: Target::Click,
179                magnitude: -0.05
180            }
181            .is_bane()
182        );
183
184        // MulFactor: <1 is bane.
185        assert!(
186            !Primitive {
187                op: Op::MulFactor,
188                target: Target::Click,
189                magnitude: 2.0
190            }
191            .is_bane()
192        );
193        assert!(
194            Primitive {
195                op: Op::MulFactor,
196                target: Target::Click,
197                magnitude: 0.5
198            }
199            .is_bane()
200        );
201
202        // CostMul: >1 is bane (more expensive).
203        assert!(
204            !Primitive {
205                op: Op::CostMul,
206                target: Target::Fingerer(0),
207                magnitude: 0.8
208            }
209            .is_bane()
210        );
211        assert!(
212            Primitive {
213                op: Op::CostMul,
214                target: Target::Fingerer(0),
215                magnitude: 1.5
216            }
217            .is_bane()
218        );
219
220        // SpawnRateMul: <1 is bane (slower spawns); >1 is boon (faster).
221        assert!(
222            !Primitive {
223                op: Op::SpawnRateMul,
224                target: Target::PowerupSpawn(PowerupKind::Lucky),
225                magnitude: 1.10
226            }
227            .is_bane()
228        );
229        assert!(
230            Primitive {
231                op: Op::SpawnRateMul,
232                target: Target::PowerupSpawn(PowerupKind::Lucky),
233                magnitude: 0.91
234            }
235            .is_bane()
236        );
237    }
238}