use rand::RngExt;
use ratatui::layout::Rect;
use crate::game::golden::{self, GoldenVariant};
use crate::game::green_coin;
use crate::game::state::{GameState, TICK_DT};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BuyQty {
One,
Ten,
Max,
}
#[derive(Clone, Debug)]
pub enum Action {
Click {
col: u16,
row: u16,
},
ClickCenter,
CatchGolden(GoldenVariant),
CatchGreenCoin,
BuyFingerer {
idx: usize,
qty: BuyQty,
},
BuyUpgrade(usize),
PrestigeReset,
UpdateGeometry {
biscuit: Rect,
},
DevAddCuques(f64),
DevForceGolden(GoldenVariant),
DevSpawnGreenCoin,
Misclick {
col: u16,
row: u16,
},
}
#[derive(Clone, Copy, Default)]
pub struct SimGeometry {
pub biscuit: Rect,
}
pub fn apply_action(state: &mut GameState, action: Action, geom: &mut SimGeometry) {
match action {
Action::Click { col, row } => {
let r = geom.biscuit;
if r.width > 0
&& col >= r.x
&& col < r.x + r.width
&& row >= r.y
&& row < r.y + r.height
{
state.click((col, row), r);
}
}
Action::ClickCenter => {
let r = geom.biscuit;
if r.width > 0 && r.height > 0 {
state.click((r.x + r.width / 2, r.y + r.height / 2), r);
}
state.space_pressed_this_tick = true;
}
Action::CatchGolden(variant) => {
state.catch_golden(variant);
}
Action::CatchGreenCoin => {
state.catch_green_coin();
}
Action::BuyFingerer { idx, qty } => match qty {
BuyQty::One => {
state.buy(idx);
}
BuyQty::Ten => {
state.buy_n(idx, 10);
}
BuyQty::Max => {
state.buy_max(idx);
}
},
Action::BuyUpgrade(idx) => {
state.buy_upgrade(idx);
}
Action::PrestigeReset => {
state.prestige_reset();
}
Action::UpdateGeometry { biscuit } => {
*geom = SimGeometry { biscuit };
}
Action::DevAddCuques(n) => {
state.dev_add_cuques(n);
}
Action::DevForceGolden(variant) => {
force_spawn_golden(state, geom, variant);
}
Action::DevSpawnGreenCoin => {
force_spawn_green_coin(state, geom);
}
Action::Misclick { col, row } => {
state.spawn_misclick(col, row);
}
}
}
pub fn sim_tick(state: &mut GameState, geom: &SimGeometry) {
state.tick();
state.tick_golden();
state.tick_green_coin();
maybe_spawn_golden(state, geom);
maybe_spawn_auto_particle(state, geom);
maybe_idle_clench(state);
}
fn maybe_idle_clench(state: &mut GameState) {
if state.clench_ticks > 0 {
return;
}
if rand::rng().random::<f64>() < 1.0 / 900.0 {
state.trigger_clench();
}
}
fn maybe_spawn_auto_particle(state: &mut GameState, geom: &SimGeometry) {
let fps = state.fps();
if fps <= 0.0 || geom.biscuit.width < 4 || geom.biscuit.height < 4 {
return;
}
let target_rate = fps.sqrt().clamp(0.5, 8.0);
let prob = target_rate * TICK_DT;
let mut rng = rand::rng();
if rng.random::<f64>() >= prob {
return;
}
let frac_x = rng.random_range(0.05_f32..=0.95);
let frac_y = rng.random_range(0.10_f32..=0.95);
state.spawn_auto_particle(frac_x, frac_y);
}
fn maybe_spawn_golden(state: &mut GameState, geom: &SimGeometry) {
if geom.biscuit.width < 8 || geom.biscuit.height < 5 {
return;
}
for variant in GoldenVariant::ALL {
let i = variant as usize;
if state.golden_cooldowns[i] > 0 || state.goldens[i].is_some() {
continue;
}
let mut g = golden::spawn_in(geom.biscuit);
g.variant = variant;
state.goldens[i] = Some(g);
state.golden_cooldowns[i] = crate::game::golden::next_cooldown();
state.goldens_since_green_coin = state.goldens_since_green_coin.saturating_add(1);
if state.green_coin.is_none() {
let p = state.goldens_since_green_coin as f64 * 0.01;
if rand::rng().random::<f64>() < p {
state.green_coin = Some(green_coin::spawn_in(geom.biscuit));
state.goldens_since_green_coin = 0;
}
}
}
}
fn force_spawn_golden(state: &mut GameState, geom: &SimGeometry, variant: GoldenVariant) {
if geom.biscuit.width < 8 || geom.biscuit.height < 5 {
return;
}
if state.goldens[variant as usize].is_some() {
return;
}
let mut g = golden::spawn_in(geom.biscuit);
g.variant = variant;
state.goldens[variant as usize] = Some(g);
}
fn force_spawn_green_coin(state: &mut GameState, geom: &SimGeometry) {
if state.green_coin.is_some() {
return;
}
if geom.biscuit.width < 8 || geom.biscuit.height < 5 {
return;
}
state.green_coin = Some(green_coin::spawn_in(geom.biscuit));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game::state::GameState;
use ratatui::layout::Rect;
fn geom_with_biscuit() -> SimGeometry {
SimGeometry {
biscuit: Rect::new(0, 0, 40, 20),
}
}
#[test]
fn force_spawn_does_not_clobber_other_variants() {
let mut state = GameState::default();
let geom = geom_with_biscuit();
force_spawn_golden(&mut state, &geom, GoldenVariant::Lucky);
force_spawn_golden(&mut state, &geom, GoldenVariant::Frenzy);
force_spawn_golden(&mut state, &geom, GoldenVariant::Buff);
assert!(state.goldens[GoldenVariant::Lucky as usize].is_some());
assert!(state.goldens[GoldenVariant::Frenzy as usize].is_some());
assert!(state.goldens[GoldenVariant::Buff as usize].is_some());
}
#[test]
fn catch_golden_only_consumes_targeted_variant() {
let mut state = GameState::default();
let geom = geom_with_biscuit();
force_spawn_golden(&mut state, &geom, GoldenVariant::Lucky);
force_spawn_golden(&mut state, &geom, GoldenVariant::Frenzy);
state.catch_golden(GoldenVariant::Lucky);
assert!(state.goldens[GoldenVariant::Lucky as usize].is_none());
assert!(state.goldens[GoldenVariant::Frenzy as usize].is_some());
}
}