1use rand::RngExt;
20use ratatui::layout::Rect;
21
22use crate::game::golden::{self, GoldenVariant};
23use crate::game::green_coin;
24use crate::game::state::{GameState, TICK_DT};
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum BuyQty {
31 One,
32 Ten,
33 Max,
34}
35
36#[derive(Clone, Debug)]
40pub enum Action {
41 Click {
42 col: u16,
43 row: u16,
44 },
45 ClickCenter,
46 CatchGolden(GoldenVariant),
50 CatchGreenCoin,
52 BuyFingerer {
53 idx: usize,
54 qty: BuyQty,
55 },
56 BuyUpgrade(usize),
57 PrestigeReset,
58 UpdateGeometry {
62 biscuit: Rect,
63 },
64 DevAddCuques(f64),
67 DevForceGolden(GoldenVariant),
68 DevSpawnGreenCoin,
71 Misclick {
75 col: u16,
76 row: u16,
77 },
78}
79
80#[derive(Clone, Copy, Default)]
83pub struct SimGeometry {
84 pub biscuit: Rect,
85}
86
87pub fn apply_action(state: &mut GameState, action: Action, geom: &mut SimGeometry) {
91 match action {
92 Action::Click { col, row } => {
93 let r = geom.biscuit;
94 if r.width > 0
95 && col >= r.x
96 && col < r.x + r.width
97 && row >= r.y
98 && row < r.y + r.height
99 {
100 state.click((col, row), r);
101 }
102 }
103 Action::ClickCenter => {
104 let r = geom.biscuit;
105 if r.width > 0 && r.height > 0 {
106 state.click((r.x + r.width / 2, r.y + r.height / 2), r);
107 }
108 state.space_pressed_this_tick = true;
113 }
114 Action::CatchGolden(variant) => {
115 state.catch_golden(variant);
116 }
117 Action::CatchGreenCoin => {
118 state.catch_green_coin();
119 }
120 Action::BuyFingerer { idx, qty } => match qty {
121 BuyQty::One => {
122 state.buy(idx);
123 }
124 BuyQty::Ten => {
125 state.buy_n(idx, 10);
126 }
127 BuyQty::Max => {
128 state.buy_max(idx);
129 }
130 },
131 Action::BuyUpgrade(idx) => {
132 state.buy_upgrade(idx);
133 }
134 Action::PrestigeReset => {
135 state.prestige_reset();
136 }
137 Action::UpdateGeometry { biscuit } => {
138 *geom = SimGeometry { biscuit };
139 }
140 Action::DevAddCuques(n) => {
141 state.dev_add_cuques(n);
142 }
143 Action::DevForceGolden(variant) => {
144 force_spawn_golden(state, geom, variant);
145 }
146 Action::DevSpawnGreenCoin => {
147 force_spawn_green_coin(state, geom);
148 }
149 Action::Misclick { col, row } => {
150 state.spawn_misclick(col, row);
151 }
152 }
153}
154
155pub fn sim_tick(state: &mut GameState, geom: &SimGeometry) {
159 state.tick();
160 state.tick_golden();
161 state.tick_green_coin();
162 maybe_spawn_golden(state, geom);
163 maybe_spawn_auto_particle(state, geom);
164 maybe_idle_clench(state);
165}
166
167fn maybe_idle_clench(state: &mut GameState) {
168 if state.clench_ticks > 0 {
169 return;
170 }
171 if rand::rng().random::<f64>() < 1.0 / 900.0 {
173 state.trigger_clench();
174 }
175}
176
177fn maybe_spawn_auto_particle(state: &mut GameState, geom: &SimGeometry) {
178 let fps = state.fps();
179 if fps <= 0.0 || geom.biscuit.width < 4 || geom.biscuit.height < 4 {
180 return;
181 }
182 let target_rate = fps.sqrt().clamp(0.5, 8.0);
183 let prob = target_rate * TICK_DT;
184 let mut rng = rand::rng();
185 if rng.random::<f64>() >= prob {
186 return;
187 }
188 let frac_x = rng.random_range(0.05_f32..=0.95);
191 let frac_y = rng.random_range(0.10_f32..=0.95);
192 state.spawn_auto_particle(frac_x, frac_y);
193}
194
195fn maybe_spawn_golden(state: &mut GameState, geom: &SimGeometry) {
196 if geom.biscuit.width < 8 || geom.biscuit.height < 5 {
197 return;
198 }
199 for variant in GoldenVariant::ALL {
204 let i = variant as usize;
205 if state.golden_cooldowns[i] > 0 || state.goldens[i].is_some() {
206 continue;
207 }
208 let mut g = golden::spawn_in(geom.biscuit);
209 g.variant = variant;
210 state.goldens[i] = Some(g);
211 state.golden_cooldowns[i] = crate::game::golden::next_cooldown();
212
213 state.goldens_since_green_coin = state.goldens_since_green_coin.saturating_add(1);
217 if state.green_coin.is_none() {
218 let p = state.goldens_since_green_coin as f64 * 0.01;
219 if rand::rng().random::<f64>() < p {
220 state.green_coin = Some(green_coin::spawn_in(geom.biscuit));
221 state.goldens_since_green_coin = 0;
222 }
223 }
224 }
225}
226
227fn force_spawn_golden(state: &mut GameState, geom: &SimGeometry, variant: GoldenVariant) {
228 if geom.biscuit.width < 8 || geom.biscuit.height < 5 {
229 return;
230 }
231 if state.goldens[variant as usize].is_some() {
235 return;
236 }
237 let mut g = golden::spawn_in(geom.biscuit);
238 g.variant = variant;
239 state.goldens[variant as usize] = Some(g);
240}
241
242fn force_spawn_green_coin(state: &mut GameState, geom: &SimGeometry) {
243 if state.green_coin.is_some() {
244 return;
245 }
246 if geom.biscuit.width < 8 || geom.biscuit.height < 5 {
247 return;
248 }
249 state.green_coin = Some(green_coin::spawn_in(geom.biscuit));
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::game::state::GameState;
256 use ratatui::layout::Rect;
257
258 fn geom_with_biscuit() -> SimGeometry {
259 SimGeometry {
260 biscuit: Rect::new(0, 0, 40, 20),
261 }
262 }
263
264 #[test]
265 fn force_spawn_does_not_clobber_other_variants() {
266 let mut state = GameState::default();
270 let geom = geom_with_biscuit();
271 force_spawn_golden(&mut state, &geom, GoldenVariant::Lucky);
272 force_spawn_golden(&mut state, &geom, GoldenVariant::Frenzy);
273 force_spawn_golden(&mut state, &geom, GoldenVariant::Buff);
274 assert!(state.goldens[GoldenVariant::Lucky as usize].is_some());
275 assert!(state.goldens[GoldenVariant::Frenzy as usize].is_some());
276 assert!(state.goldens[GoldenVariant::Buff as usize].is_some());
277 }
278
279 #[test]
280 fn catch_golden_only_consumes_targeted_variant() {
281 let mut state = GameState::default();
282 let geom = geom_with_biscuit();
283 force_spawn_golden(&mut state, &geom, GoldenVariant::Lucky);
284 force_spawn_golden(&mut state, &geom, GoldenVariant::Frenzy);
285 state.catch_golden(GoldenVariant::Lucky);
287 assert!(state.goldens[GoldenVariant::Lucky as usize].is_none());
288 assert!(state.goldens[GoldenVariant::Frenzy as usize].is_some());
289 }
290}