use ratatui::{prelude::*, widgets::*};
use crate::game::powerup::{Powerup, PowerupKind};
use crate::game::state::{Buff, CLENCH_SQUASH_TICKS, CLENCH_TICKS, GameState};
const SPIN_FRAMES: [char; 5] = ['\\', '|', '/', '-', '*'];
fn prestige_body_tint(prestige: u64) -> ((f32, f32, f32), f32) {
match prestige {
0 => ((220.0, 170.0, 150.0), 0.0),
1..=2 => ((255.0, 200.0, 110.0), 0.18), 3..=5 => ((255.0, 215.0, 80.0), 0.32), 6..=9 => ((230.0, 220.0, 235.0), 0.40), 10..=14 => ((180.0, 230.0, 255.0), 0.50), 15..=24 => ((220.0, 200.0, 255.0), 0.55), _ => ((255.0, 250.0, 240.0), 0.65), }
}
const BISCUIT_FULL: &[&str] = &[
r" ____________________ ",
r" __,-~~ ~~-,__ ",
r" ,-~' `~-, ",
r" ,-' `-, ",
r" ,' `. ",
r" / -~-~-~- -~-~-~- \ ",
r" / \ ",
r" / -~~-~-~~- \ ",
r" / \ ",
r" | -~-~-~-~- -~-~-~-~- |",
r" | |",
r" | |",
r" | \\\\\\\\ | //////// |",
r" | \\\\\\\\ | //////// |",
r" | \\\\\\\\\|///////// |",
r" | ~ - - - - - - - - - - ~ |",
r" | /////////|\\\\\\\\\ |",
r" | //////// | \\\\\\\\ |",
r" | //////// | \\\\\\\\ |",
r" | |",
r" | -~-~-~-~- -~-~-~-~- |",
r" \ / ",
r" \ -~~-~-~~- / ",
r" \ / ",
r" \ -~-~-~- -~-~-~- / ",
r" `. ,' ",
r" `-, ,-' ",
r" `~-, ,-~' ",
r" `~-,,_ _,,-~' ",
r" `~-,,______________,,-~' ",
];
const BISCUIT_MEDIUM: &[&str] = &[
r" ________________ ",
r" ,-~ ~-, ",
r" ,-' `-, ",
r" ,' `. ",
r" / -~-~- -~-~- \ ",
r" / \",
r" | \\\\\ | ///// |",
r" | \\\\\ | ///// |",
r" | \\\\\\|////// |",
r" | ~ - - - - - - ~ |",
r" | //////|\\\\\\ |",
r" | ///// | \\\\\ |",
r" | ///// | \\\\\ |",
r" \ /",
r" \ -~-~- -~-~- / ",
r" `. ,' ",
r" `-, ,-' ",
r" `~-,,_______________,,-~' ",
];
const BISCUIT_SMALL: &[&str] = &[
r" ___________ ",
r" ,-~ ~-, ",
r" ,' `. ",
r" / -~-~- -~-~- \ ",
r" | \\\ | /// | ",
r" | \\\|/// | ",
r" | ~ - - - - ~ | ",
r" | ///|\\\ | ",
r" | /// | \\\ | ",
r" \ -~-~- -~-~- / ",
r" `. ,' ",
r" `-,,_________,,-' ",
];
pub(crate) const BISCUIT_TINY: &[&str] = &[
r" ______ ",
r" ,~ ~, ",
r" / \ ",
r" | \|/ | ",
r" | - - | ",
r" | /|\ | ",
r" \ / ",
r" `-,____,-' ",
];
pub(crate) const BISCUIT_TINY_FOCAL: (u16, u16) = (7, 4);
struct BiscuitArt {
rows: &'static [&'static str],
asshole_col: u16,
asshole_row: u16,
label: Option<&'static str>,
}
const BISCUIT_LEVELS: &[BiscuitArt] = &[
BiscuitArt {
rows: BISCUIT_FULL,
asshole_col: 31,
asshole_row: 15,
label: None,
},
BiscuitArt {
rows: BISCUIT_MEDIUM,
asshole_col: 20,
asshole_row: 9,
label: Some("70%"),
},
BiscuitArt {
rows: BISCUIT_SMALL,
asshole_col: 13,
asshole_row: 6,
label: Some("45%"),
},
BiscuitArt {
rows: BISCUIT_TINY,
asshole_col: 7,
asshole_row: 4,
label: Some("25%"),
},
];
pub fn level_count() -> usize {
BISCUIT_LEVELS.len()
}
pub fn focal_point(zoom_idx: usize, biscuit: Rect) -> (u16, u16) {
let level = &BISCUIT_LEVELS[zoom_idx.min(BISCUIT_LEVELS.len() - 1)];
(biscuit.x + level.asshole_col, biscuit.y + level.asshole_row)
}
pub fn level_label(idx: usize) -> Option<&'static str> {
BISCUIT_LEVELS.get(idx).and_then(|a| a.label)
}
pub fn draw(frame: &mut Frame, area: Rect, state: &GameState, zoom_idx: usize) -> Rect {
let level = &BISCUIT_LEVELS[zoom_idx.min(BISCUIT_LEVELS.len() - 1)];
let art = level.rows;
let clenched = state.clench_ticks > 0;
let squash = clenched && state.clench_ticks + CLENCH_SQUASH_TICKS > CLENCH_TICKS;
let render_art_owned: Vec<String> = if squash {
squashed_art(art, level.asshole_row as usize)
} else {
art.iter().map(|s| s.to_string()).collect()
};
let render_art: Vec<&str> = render_art_owned.iter().map(|s| s.as_str()).collect();
let w = render_art
.iter()
.map(|s| s.chars().count())
.max()
.unwrap_or(0) as u16;
let h = render_art.len() as u16;
let target_asshole_col = area.x + area.width / 2;
let x_base = target_asshole_col
.saturating_sub(level.asshole_col)
.max(area.x)
.min((area.x + area.width).saturating_sub(w));
let y_base = area.y + area.height.saturating_sub(h) / 2;
let stable_rect = Rect {
x: x_base,
y: y_base,
width: w.min(area.width),
height: h.min(area.height.saturating_sub(y_base - area.y)),
};
let frenzy_active = state
.buffs
.iter()
.any(|b| matches!(b, Buff::ClickFrenzy { .. }));
let shake = if frenzy_active && clenched {
(state.session_ticks % 3) as i32 - 1
} else {
0
};
let render_x = ((x_base as i32 + shake)
.max(area.x as i32)
.min((area.x + area.width).saturating_sub(stable_rect.width) as i32))
as u16;
let render_rect = Rect {
x: render_x,
y: stable_rect.y,
width: stable_rect.width,
height: stable_rect.height,
};
let lines: Vec<Line> = render_art
.iter()
.map(|s| Line::from(s.to_string()))
.collect();
let base = if clenched {
if frenzy_active {
(255.0_f32, 80.0, 110.0)
} else {
(255.0_f32, 120.0, 140.0)
}
} else {
let t = (state.session_ticks as f32) / 25.0; let breath = 1.0 + 0.05 * t.sin();
let (tint, mix) = prestige_body_tint(state.prestige);
let base_r = 220.0 * breath;
let base_g = 170.0 * breath;
let base_b = 150.0 * breath;
let r = base_r + (tint.0 - base_r) * mix;
let g = base_g + (tint.1 - base_g) * mix;
let b = base_b + (tint.2 - base_b) * mix;
(
r.clamp(0.0, 255.0),
g.clamp(0.0, 255.0),
b.clamp(0.0, 255.0),
)
};
let color = Color::Rgb(base.0 as u8, base.1 as u8, base.2 as u8);
let p = Paragraph::new(lines).style(Style::default().fg(color));
frame.render_widget(p, render_rect);
let space_held = state.space_held();
let asshole_glyph: char = if !clenched {
'O'
} else if space_held {
SPIN_FRAMES[(state.total_clicks as usize) % SPIN_FRAMES.len()]
} else {
'*'
};
let buf = frame.buffer_mut();
let cx = render_rect.x + level.asshole_col;
let cy = render_rect.y + level.asshole_row;
if cx < buf.area.x + buf.area.width && cy < buf.area.y + buf.area.height {
let style = if clenched {
let phase = (state.session_ticks as f32) * 0.8;
let pulse = (phase.sin() + 1.0) * 0.5;
let r = (200.0 + 55.0 * pulse) as u8;
let g = (30.0 + 60.0 * pulse) as u8;
Style::default()
.fg(Color::Rgb(r, g, 0))
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(color)
};
let cell = &mut buf[(cx, cy)];
cell.set_char(asshole_glyph);
cell.set_style(style);
}
stable_rect
}
fn squashed_art(art: &[&str], asshole_row: usize) -> Vec<String> {
let n = art.len();
if n < 5 || asshole_row == 0 || asshole_row + 1 >= n {
return art.iter().map(|s| s.to_string()).collect();
}
let width = art.iter().map(|s| s.chars().count()).max().unwrap_or(0);
let blank: String = " ".repeat(width);
let mut out: Vec<String> = Vec::with_capacity(n);
out.push(blank.clone()); for s in art.iter().take(asshole_row - 1) {
out.push((*s).to_string());
}
out.push(art[asshole_row].to_string());
for s in art.iter().skip(asshole_row + 2) {
out.push((*s).to_string());
}
out.push(blank); debug_assert_eq!(out.len(), n);
out
}
struct PowerupPalette {
center: char,
bright: (f32, f32, f32),
dim: (f32, f32, f32),
accent: (f32, f32, f32),
bg: Color,
}
fn powerup_palette(kind: PowerupKind) -> PowerupPalette {
match kind {
PowerupKind::Lucky => PowerupPalette {
center: '$',
bright: (255.0, 230.0, 80.0),
dim: (140.0, 90.0, 0.0),
accent: (255.0, 170.0, 30.0),
bg: Color::Rgb(40, 25, 0),
},
PowerupKind::Frenzy => PowerupPalette {
center: '!',
bright: (255.0, 110.0, 110.0),
dim: (120.0, 0.0, 0.0),
accent: (255.0, 200.0, 60.0),
bg: Color::Rgb(50, 0, 0),
},
PowerupKind::Buff => PowerupPalette {
center: '+',
bright: (230.0, 160.0, 255.0),
dim: (80.0, 20.0, 110.0),
accent: (140.0, 220.0, 255.0),
bg: Color::Rgb(35, 0, 45),
},
PowerupKind::GreenCoin => PowerupPalette {
center: '$',
bright: (140.0, 255.0, 160.0),
dim: (10.0, 80.0, 30.0),
accent: (200.0, 255.0, 110.0),
bg: Color::Rgb(0, 30, 10),
},
}
}
pub fn draw_powerup(frame: &mut Frame, powerup: &Powerup, biscuit: Rect) -> Rect {
let buf = frame.buffer_mut();
let PowerupPalette {
center,
bright,
dim,
accent,
bg,
} = powerup_palette(powerup.kind);
let life_total = powerup.kind.lifetime_ticks();
let life_frac = (powerup.life_ticks as f32 / life_total as f32).clamp(0.0, 1.0);
let alarm = life_frac < 0.20;
let speed = if alarm { 1.5 } else { 0.6 };
let dim_pull = if alarm { 1.0 } else { 0.6 };
let phase = (life_total - powerup.life_ticks) as f32 * speed;
let cell_offset = std::f32::consts::TAU / 5.0;
let lines: [String; 3] = [
".---.".to_string(),
format!("( {} )", center),
"`---'".to_string(),
];
let w: u16 = 5;
let h: u16 = 3;
let area = buf.area;
if area.width == 0 || area.height == 0 || biscuit.width < w || biscuit.height < h {
return Rect::default();
}
let (anchor_col, anchor_row) =
crate::game::state::biscuit_frac_to_screen(powerup.frac_x, powerup.frac_y, biscuit);
let mut col = anchor_col;
let mut row = anchor_row;
if col + w > biscuit.x + biscuit.width {
col = (biscuit.x + biscuit.width).saturating_sub(w);
}
if row + h > biscuit.y + biscuit.height {
row = (biscuit.y + biscuit.height).saturating_sub(h);
}
if col < biscuit.x {
col = biscuit.x;
}
if row < biscuit.y {
row = biscuit.y;
}
if col + w > area.x + area.width {
col = (area.x + area.width).saturating_sub(w);
}
if row + h > area.y + area.height {
row = (area.y + area.height).saturating_sub(h);
}
for (dy, line) in lines.iter().enumerate() {
let y = row + dy as u16;
if y >= area.y + area.height {
break;
}
for (i, ch) in line.chars().enumerate() {
let x = col + i as u16;
if x >= area.x + area.width {
break;
}
let arg = phase + i as f32 * cell_offset;
let wave_main = (arg.sin() + 1.0) * 0.5; let wave_accent = ((arg + std::f32::consts::FRAC_PI_2).sin() + 1.0) * 0.5;
let dim_dim = (
dim.0 * (1.0 - 0.4 * dim_pull),
dim.1 * (1.0 - 0.4 * dim_pull),
dim.2 * (1.0 - 0.4 * dim_pull),
);
let main_r = dim_dim.0 + (bright.0 - dim_dim.0) * wave_main;
let main_g = dim_dim.1 + (bright.1 - dim_dim.1) * wave_main;
let main_b = dim_dim.2 + (bright.2 - dim_dim.2) * wave_main;
let accent_w = wave_accent * 0.35;
let r = main_r + (accent.0 - main_r) * accent_w;
let g = main_g + (accent.1 - main_g) * accent_w;
let b = main_b + (accent.2 - main_b) * accent_w;
let style = Style::default()
.fg(Color::Rgb(
r.clamp(0.0, 255.0) as u8,
g.clamp(0.0, 255.0) as u8,
b.clamp(0.0, 255.0) as u8,
))
.bg(bg)
.add_modifier(Modifier::BOLD);
buf.set_string(x, y, ch.to_string(), style);
}
}
Rect {
x: col,
y: row,
width: w,
height: h,
}
}