1use anyhow::Result;
21use arc_swap::ArcSwap;
22use crossterm::event::{
23 self, Event, KeyCode as CtKeyCode, KeyEventKind, KeyModifiers, MouseButton as CtMouseButton,
24 MouseEvent as CtMouseEvent, MouseEventKind,
25};
26use rand::RngExt;
27use ratatui::{Terminal, prelude::*};
28use std::sync::{
29 Arc,
30 atomic::{AtomicBool, Ordering},
31 mpsc,
32};
33use std::thread;
34use std::time::{Duration, Instant};
35
36use crate::game::achievement::ACHIEVEMENTS;
37use crate::game::fingerer;
38use crate::game::fingerer::FINGERERS;
39use crate::game::golden::{self, GoldenVariant};
40use crate::game::state::{GameState, TICK_HZ};
41use crate::game::upgrade::UPGRADES;
42use crate::input::{
43 self, InputContext, InputEvent, KeyCode as InKeyCode, Modifiers, MouseButton as InMouseButton,
44 UiState, WheelDelta,
45};
46use crate::platform::Persistence;
47use crate::sim::{self, Action, SimGeometry};
48use crate::ui::{self, Mode};
49
50const SAVE_INTERVAL_TICKS: u64 = TICK_HZ as u64 * 10;
51const DEMO_GOLDEN_COOLDOWN: u32 = 40;
54const INPUT_POLL_MS: u64 = 16;
58const MAX_TICK_CATCHUP: u32 = 20;
62
63enum SimMsg {
67 DemoSetMode(Mode),
68 DemoQuit,
69}
70
71pub struct App {
72 state: GameState,
73 debug: bool,
74 demo_seconds: Option<u32>,
75 persistence: Persistence,
76}
77
78impl App {
79 pub fn new(
80 state: GameState,
81 debug: bool,
82 demo_seconds: Option<u32>,
83 persistence: Persistence,
84 ) -> Self {
85 Self {
86 state,
87 debug,
88 demo_seconds,
89 persistence,
90 }
91 }
92
93 pub fn run<B: Backend>(self, terminal: &mut Terminal<B>) -> Result<()>
94 where
95 B::Error: Send + Sync + 'static,
96 {
97 let App {
98 state,
99 debug,
100 demo_seconds,
101 persistence,
102 } = self;
103
104 let snapshot = Arc::new(ArcSwap::from_pointee(state.clone()));
105 let shutdown = Arc::new(AtomicBool::new(false));
106 let (action_tx, action_rx) = mpsc::channel::<Action>();
107 let (sim_msg_tx, sim_msg_rx) = mpsc::channel::<SimMsg>();
108
109 let sim_handle = {
110 let snapshot = snapshot.clone();
111 let shutdown = shutdown.clone();
112 thread::Builder::new()
113 .name("cuque-sim".into())
114 .spawn(move || {
115 sim_loop(
116 state,
117 snapshot,
118 action_rx,
119 sim_msg_tx,
120 shutdown,
121 demo_seconds,
122 persistence,
123 );
124 })
125 .expect("spawn sim thread")
126 };
127
128 let mut ui = UiState::new();
129 let mut upgrade_rows: Vec<(usize, Rect)> = Vec::new();
130 let mut fingerer_rows: Vec<(usize, Rect)> = Vec::new();
131 let mut biscuit_rect = Rect::default();
132 let mut golden_rect = Rect::default();
133 let mut play_area = Rect::default();
134 let mut help_hits: Vec<(crate::ui::HelpAction, Rect)> = Vec::new();
139 let mut prestige_reset_rect = Rect::default();
140 let mut actions: Vec<Action> = Vec::with_capacity(4);
143
144 while ui.running && !shutdown.load(Ordering::Relaxed) {
145 for msg in sim_msg_rx.try_iter() {
148 match msg {
149 SimMsg::DemoSetMode(m) => ui.mode = m,
150 SimMsg::DemoQuit => ui.running = false,
151 }
152 }
153
154 let current = snapshot.load_full();
155 terminal.draw(|f| {
156 let out = ui::draw(f, ¤t, ui.mode, ui.zoom_idx, debug, ui.last_mouse_pos);
157 biscuit_rect = out.biscuit_rect;
158 golden_rect = out.golden_rect;
159 play_area = out.play_area;
160 upgrade_rows = out.upgrade_rows;
161 fingerer_rows = out.fingerer_rows;
162 help_hits = out.help_hits;
163 prestige_reset_rect = out.prestige_reset_rect;
164 })?;
165
166 let _ = action_tx.send(Action::UpdateGeometry {
169 biscuit: biscuit_rect,
170 });
171
172 if event::poll(Duration::from_millis(INPUT_POLL_MS))? {
173 let ctx = InputContext {
174 fingerer_rows: &fingerer_rows,
175 upgrade_rows: &upgrade_rows,
176 help_hits: &help_hits,
177 biscuit_rect,
178 golden_rect,
179 play_area,
180 prestige_reset_rect,
181 debug,
182 current: ¤t,
183 };
184 loop {
185 let ev = event::read()?;
186 if let Some(input_ev) = translate_crossterm(ev) {
187 actions.clear();
188 input::process_input_event(input_ev, &mut ui, &ctx, &mut actions);
189 for a in actions.drain(..) {
190 let _ = action_tx.send(a);
191 }
192 }
193 if !event::poll(Duration::ZERO)? {
194 break;
195 }
196 }
197 }
198 }
199
200 shutdown.store(true, Ordering::Relaxed);
202 drop(action_tx);
203 sim_handle.join().expect("sim thread panicked");
204 Ok(())
205 }
206}
207
208fn sim_loop(
211 mut state: GameState,
212 snapshot: Arc<ArcSwap<GameState>>,
213 actions: mpsc::Receiver<Action>,
214 sim_msg_tx: mpsc::Sender<SimMsg>,
215 shutdown: Arc<AtomicBool>,
216 demo_seconds: Option<u32>,
217 persistence: Persistence,
218) {
219 let tick_dt = Duration::from_micros(1_000_000 / TICK_HZ as u64);
220 let mut next_tick = Instant::now() + tick_dt;
221 let mut ticks_since_save: u64 = 0;
222 let mut demo_ticks: u64 = 0;
223 let mut demo_golden_spawns: u32 = 0;
224 let mut geom = SimGeometry::default();
225
226 loop {
227 if shutdown.load(Ordering::Relaxed) {
228 break;
229 }
230
231 let timeout = next_tick.saturating_duration_since(Instant::now());
234 match actions.recv_timeout(timeout) {
235 Ok(action) => sim::apply_action(&mut state, action, &mut geom),
236 Err(mpsc::RecvTimeoutError::Timeout) => {}
237 Err(mpsc::RecvTimeoutError::Disconnected) => break,
238 }
239
240 let mut catchup = 0u32;
244 while Instant::now() >= next_tick {
245 sim::sim_tick(&mut state, &geom);
246 if demo_seconds.is_some() {
250 demo_driver_tick(
251 &mut state,
252 &geom,
253 demo_seconds,
254 &mut demo_ticks,
255 &mut demo_golden_spawns,
256 &sim_msg_tx,
257 );
258 } else {
259 ticks_since_save += 1;
260 if ticks_since_save >= SAVE_INTERVAL_TICKS {
261 ticks_since_save = 0;
262 let _ = persistence.save(&state);
263 }
264 }
265 next_tick += tick_dt;
266 catchup += 1;
267 if catchup >= MAX_TICK_CATCHUP && Instant::now() > next_tick {
268 next_tick = Instant::now() + tick_dt;
269 break;
270 }
271 }
272
273 snapshot.store(Arc::new(state.clone()));
276 }
277
278 if demo_seconds.is_none() {
281 state.tick_achievements();
282 let _ = persistence.save(&state);
283 }
284}
285
286fn demo_driver_tick(
290 state: &mut GameState,
291 geom: &SimGeometry,
292 demo_seconds: Option<u32>,
293 demo_ticks: &mut u64,
294 demo_golden_spawns: &mut u32,
295 sim_msg_tx: &mpsc::Sender<SimMsg>,
296) {
297 *demo_ticks += 1;
298 let t = *demo_ticks;
299 let mut rng = rand::rng();
300
301 if t.is_multiple_of(13) {
303 let r = geom.biscuit;
304 if r.width > 0 && r.height > 0 {
305 state.click((r.x + r.width / 2, r.y + r.height / 2), r);
306 }
307 }
308
309 if state.golden.is_none() && state.golden_cooldown == 0 {
311 state.golden_cooldown = DEMO_GOLDEN_COOLDOWN;
312 }
313
314 if let Some(g) = &mut state.golden
318 && g.life_ticks == golden::GOLDEN_LIFE_TICKS
319 {
320 g.variant = match *demo_golden_spawns % 3 {
321 0 => GoldenVariant::Buff,
322 1 => GoldenVariant::Frenzy,
323 _ => GoldenVariant::Lucky,
324 };
325 *demo_golden_spawns += 1;
326 }
327
328 if let Some(g) = &state.golden
331 && g.life_ticks + 20 < golden::GOLDEN_LIFE_TICKS
332 {
333 state.catch_golden();
334 }
335
336 if t.is_multiple_of(80) {
338 let candidates: Vec<usize> = (0..fingerer::count())
339 .filter(|&i| state.can_buy(i))
340 .collect();
341 if !candidates.is_empty() {
342 let idx = candidates[rng.random_range(0..candidates.len())];
343 state.buy_n(idx, rng.random_range(1..=2));
344 }
345 }
346
347 if t.is_multiple_of(160) {
349 let available = crate::game::upgrade::available_ids(state);
350 if let Some(&u_idx) = available
351 .iter()
352 .min_by(|&&a, &&b| UPGRADES[a].cost.partial_cmp(&UPGRADES[b].cost).unwrap())
353 {
354 state.buy_upgrade(u_idx);
355 }
356 }
357
358 let phase = t % 300;
360 let panel_swap = if phase == 100 {
361 Some(Mode::Stats)
362 } else if phase == 140 {
363 Some(Mode::Achievements)
364 } else if phase == 180 {
365 Some(Mode::Upgrades)
366 } else if phase == 220 {
367 Some(Mode::Game)
368 } else {
369 None
370 };
371 if let Some(m) = panel_swap {
372 let _ = sim_msg_tx.send(SimMsg::DemoSetMode(m));
373 }
374
375 if let Some(secs) = demo_seconds
378 && t >= (secs as u64) * (TICK_HZ as u64)
379 {
380 let _ = sim_msg_tx.send(SimMsg::DemoQuit);
381 }
382}
383
384fn translate_crossterm(ev: Event) -> Option<InputEvent> {
390 match ev {
391 Event::Key(k) if k.kind == KeyEventKind::Press => {
392 let code = translate_key_code(k.code)?;
393 Some(InputEvent::KeyPress {
394 code,
395 mods: translate_mods(k.modifiers),
396 })
397 }
398 Event::Mouse(m) => translate_mouse(m),
399 _ => None,
400 }
401}
402
403fn translate_key_code(code: CtKeyCode) -> Option<InKeyCode> {
404 match code {
405 CtKeyCode::Char(c) => Some(InKeyCode::Char(c)),
406 CtKeyCode::Esc => Some(InKeyCode::Esc),
407 CtKeyCode::F(n) => Some(InKeyCode::F(n)),
408 _ => None,
409 }
410}
411
412fn translate_mods(mods: KeyModifiers) -> Modifiers {
413 Modifiers {
414 shift: mods.contains(KeyModifiers::SHIFT),
415 alt: mods.contains(KeyModifiers::ALT),
416 ctrl: mods.contains(KeyModifiers::CONTROL),
417 }
418}
419
420fn translate_mouse_button(button: CtMouseButton) -> Option<InMouseButton> {
425 match button {
426 CtMouseButton::Left => Some(InMouseButton::Left),
427 CtMouseButton::Right => Some(InMouseButton::Right),
428 CtMouseButton::Middle => None,
429 }
430}
431
432fn translate_mouse(m: CtMouseEvent) -> Option<InputEvent> {
433 let mods = translate_mods(m.modifiers);
434 match m.kind {
435 MouseEventKind::Down(button) => Some(InputEvent::MouseDown {
436 col: m.column,
437 row: m.row,
438 button: translate_mouse_button(button)?,
439 mods,
440 }),
441 MouseEventKind::ScrollUp => Some(InputEvent::Wheel {
442 col: m.column,
443 row: m.row,
444 delta: WheelDelta::Up,
445 }),
446 MouseEventKind::ScrollDown => Some(InputEvent::Wheel {
447 col: m.column,
448 row: m.row,
449 delta: WheelDelta::Down,
450 }),
451 MouseEventKind::Moved | MouseEventKind::Drag(CtMouseButton::Left) => {
456 Some(InputEvent::MouseMoved {
457 col: m.column,
458 row: m.row,
459 })
460 }
461 _ => None,
462 }
463}
464
465pub fn build_demo_state() -> GameState {
474 let mut s = GameState {
475 cuques: 500_000.0,
478 lifetime_cuques: 500_000_000.0, total_clicks: 500,
480 total_play_ticks: 3600 * TICK_HZ as u64, prestige: 3,
482 golden_caught: 7,
483 golden_cooldown: 0,
487 best_fps: 50_000.0,
488 ..GameState::default()
489 };
490 const DEMO_FINGERER_COUNTS: &[u32] = &[40, 40, 35, 30, 25, 20, 15, 10];
498 for (idx, &count) in DEMO_FINGERER_COUNTS.iter().enumerate() {
499 if let Some(f) = FINGERERS.get(idx)
500 && count > 0
501 {
502 s.fingerers_owned.insert(f.id.to_string(), count);
503 }
504 }
505 for u in UPGRADES.iter().take(10) {
509 s.upgrades_earned.insert(u.id.to_string());
510 }
511 for a in ACHIEVEMENTS.iter().take(6) {
513 s.achievements_earned.insert(a.id.to_string());
514 }
515 s
516}