1use ratatui::{prelude::*, widgets::*};
2
3use crate::game::state::{
4 ACHIEVEMENT_FLASH_TICKS, Buff, GameState, LUCKY_FLASH_TICKS, PURCHASE_FLASH_TICKS,
5};
6
7const BASELINE: (f32, f32, f32) = (200.0, 200.0, 210.0);
9const ACTIVE_CARRIER: (f32, f32, f32) = (255.0, 255.0, 255.0);
12
13const FRENZY_TINT: (f32, f32, f32) = (255.0, 60.0, 60.0);
19const FRENZY_CYCLE: f32 = 13.0;
20const BUFF_TINT: (f32, f32, f32) = (200.0, 100.0, 255.0);
21const BUFF_CYCLE: f32 = 17.0;
22const LUCKY_TINT: (f32, f32, f32) = (255.0, 215.0, 0.0);
23const LUCKY_CYCLE: f32 = 23.0;
24const PURCHASE_TINT: (f32, f32, f32) = (40.0, 230.0, 80.0);
25const PURCHASE_CYCLE: f32 = 11.0;
26const ACHIEVEMENT_TINT: (f32, f32, f32) = (255.0, 200.0, 100.0);
30const ACHIEVEMENT_CYCLE: f32 = 19.0;
31
32const PRESTIGE_TINT: (f32, f32, f32) = (200.0, 170.0, 80.0);
34const PRESTIGE_WEIGHT: f32 = 0.08;
35
36pub fn draw_animated(frame: &mut Frame, area: Rect, state: &GameState, title: &str) {
37 let block = Block::bordered().title(title);
38 frame.render_widget(block, area);
39
40 if area.width < 2 || area.height < 2 {
41 return;
42 }
43
44 let buf = frame.buffer_mut();
45 let mut i: usize = 0;
46 let last_x = area.x + area.width - 1;
47 let last_y = area.y + area.height - 1;
48
49 for x in area.x..=last_x {
50 recolor(buf, x, area.y, cell_color(i, state));
51 i += 1;
52 }
53 for y in (area.y + 1)..=last_y {
54 recolor(buf, last_x, y, cell_color(i, state));
55 i += 1;
56 }
57 if area.height > 1 {
58 for x in (area.x..last_x).rev() {
59 recolor(buf, x, last_y, cell_color(i, state));
60 i += 1;
61 }
62 }
63 if area.width > 1 && area.height > 2 {
64 for y in ((area.y + 1)..last_y).rev() {
65 recolor(buf, area.x, y, cell_color(i, state));
66 i += 1;
67 }
68 }
69}
70
71fn recolor(buf: &mut Buffer, x: u16, y: u16, color: Color) {
72 if x >= buf.area.x + buf.area.width || y >= buf.area.y + buf.area.height {
73 return;
74 }
75 let cell = &mut buf[(x, y)];
76 cell.set_fg(color);
77 cell.modifier.insert(Modifier::BOLD);
78}
79
80fn cell_color(i: usize, state: &GameState) -> Color {
81 let phase = state.border_phase as f32;
82
83 let purchase_s = plateau_fade(state.purchase_flash_ticks, PURCHASE_FLASH_TICKS);
90 let lucky_s = plateau_fade(state.lucky_flash_ticks, LUCKY_FLASH_TICKS);
91 let achievement_s = plateau_fade(state.achievement_flash_ticks, ACHIEVEMENT_FLASH_TICKS);
92 let frenzy_s = state
93 .buffs
94 .iter()
95 .filter_map(|b| match b {
96 Buff::ClickFrenzy { .. } => Some(b.strength()),
97 _ => None,
98 })
99 .fold(0.0_f32, f32::max);
100 let buff_s = state
101 .buffs
102 .iter()
103 .filter_map(|b| match b {
104 Buff::FingererBoost { .. } => Some(b.strength()),
105 _ => None,
106 })
107 .fold(0.0_f32, f32::max);
108
109 let activity = purchase_s
114 .max(lucky_s)
115 .max(achievement_s)
116 .max(frenzy_s)
117 .max(buff_s);
118 let carrier_r = BASELINE.0 + (ACTIVE_CARRIER.0 - BASELINE.0) * activity;
119 let carrier_g = BASELINE.1 + (ACTIVE_CARRIER.1 - BASELINE.1) * activity;
120 let carrier_b = BASELINE.2 + (ACTIVE_CARRIER.2 - BASELINE.2) * activity;
121
122 let mut r = carrier_r;
123 let mut g = carrier_g;
124 let mut b = carrier_b;
125
126 let purchase_amp = state.purchase_flash_strength.clamp(1.0, 3.0);
135 for (tint, cycle, strength, amp) in [
136 (PURCHASE_TINT, PURCHASE_CYCLE, purchase_s, purchase_amp),
137 (LUCKY_TINT, LUCKY_CYCLE, lucky_s, 1.0),
138 (ACHIEVEMENT_TINT, ACHIEVEMENT_CYCLE, achievement_s, 1.0),
139 (BUFF_TINT, BUFF_CYCLE, buff_s, 1.0),
140 (FRENZY_TINT, FRENZY_CYCLE, frenzy_s, 1.0),
141 ] {
142 if strength > 0.001 {
143 let wave01 = (((i as f32 + phase) * std::f32::consts::TAU / cycle).sin() + 1.0) * 0.5;
144 let contribution = (wave01 * strength * amp).min(1.0);
145 r += (tint.0 - carrier_r) * contribution;
146 g += (tint.1 - carrier_g) * contribution;
147 b += (tint.2 - carrier_b) * contribution;
148 }
149 }
150
151 let mut r = r.clamp(0.0, 255.0);
152 let mut g = g.clamp(0.0, 255.0);
153 let mut b = b.clamp(0.0, 255.0);
154
155 if state.prestige > 0 {
156 r += (PRESTIGE_TINT.0 - r) * PRESTIGE_WEIGHT;
157 g += (PRESTIGE_TINT.1 - g) * PRESTIGE_WEIGHT;
158 b += (PRESTIGE_TINT.2 - b) * PRESTIGE_WEIGHT;
159 }
160
161 Color::Rgb(r as u8, g as u8, b as u8)
162}
163
164fn smoothstep(t: f32) -> f32 {
165 let t = t.clamp(0.0, 1.0);
166 t * t * (3.0 - 2.0 * t)
167}
168
169pub fn plateau_fade(remaining: u32, total: u32) -> f32 {
172 if total == 0 {
173 return 0.0;
174 }
175 let fade_ticks = (total as f32 * 0.4).max(1.0);
176 let r = remaining as f32;
177 if r >= fade_ticks {
178 1.0
179 } else {
180 smoothstep(r / fade_ticks)
181 }
182}
183
184pub fn paint_border_flash(
193 frame: &mut Frame,
194 area: Rect,
195 state: &GameState,
196 tint: (f32, f32, f32),
197 cycle: f32,
198 strength: f32,
199) {
200 if strength <= 0.001 || area.width < 2 || area.height < 2 {
201 return;
202 }
203 let buf = frame.buffer_mut();
204 let phase = state.steady_phase as f32;
209 let last_x = area.x + area.width - 1;
210 let last_y = area.y + area.height - 1;
211
212 let mut paint = |x: u16, y: u16, i: usize| {
213 if x >= buf.area.x + buf.area.width || y >= buf.area.y + buf.area.height {
214 return;
215 }
216 let wave01 = (((i as f32 + phase) * std::f32::consts::TAU / cycle).sin() + 1.0) * 0.5;
217 let carrier_r = BASELINE.0 + (ACTIVE_CARRIER.0 - BASELINE.0) * strength;
220 let carrier_g = BASELINE.1 + (ACTIVE_CARRIER.1 - BASELINE.1) * strength;
221 let carrier_b = BASELINE.2 + (ACTIVE_CARRIER.2 - BASELINE.2) * strength;
222 let contribution = wave01 * strength;
223 let r = carrier_r + (tint.0 - carrier_r) * contribution;
224 let g = carrier_g + (tint.1 - carrier_g) * contribution;
225 let b = carrier_b + (tint.2 - carrier_b) * contribution;
226 let cell = &mut buf[(x, y)];
227 cell.set_fg(Color::Rgb(
228 r.clamp(0.0, 255.0) as u8,
229 g.clamp(0.0, 255.0) as u8,
230 b.clamp(0.0, 255.0) as u8,
231 ));
232 cell.modifier.insert(Modifier::BOLD);
233 };
234
235 let mut i = 0usize;
236 for x in area.x..=last_x {
237 paint(x, area.y, i);
238 i += 1;
239 }
240 for y in (area.y + 1)..=last_y {
241 paint(last_x, y, i);
242 i += 1;
243 }
244 if area.height > 1 {
245 for x in (area.x..last_x).rev() {
246 paint(x, last_y, i);
247 i += 1;
248 }
249 }
250 if area.width > 1 && area.height > 2 {
251 for y in ((area.y + 1)..last_y).rev() {
252 paint(area.x, y, i);
253 i += 1;
254 }
255 }
256}
257
258pub const PANEL_PURCHASE_TINT: (f32, f32, f32) = PURCHASE_TINT;
263pub const PANEL_PURCHASE_CYCLE: f32 = PURCHASE_CYCLE;
264pub const PANEL_UNAFFORDABLE_TINT: (f32, f32, f32) = (255.0, 70.0, 70.0);
265pub const PANEL_UNAFFORDABLE_CYCLE: f32 = 7.0;
266pub const PANEL_ACHIEVEMENT_TINT: (f32, f32, f32) = ACHIEVEMENT_TINT;
267pub const PANEL_ACHIEVEMENT_CYCLE: f32 = ACHIEVEMENT_CYCLE;