1use ratatui::{prelude::*, widgets::*};
2
3use crate::game::state::{
4 ACHIEVEMENT_FLASH_TICKS, Buff, GREEN_COIN_FLASH_TICKS, GameState, LUCKY_FLASH_TICKS,
5 PURCHASE_FLASH_TICKS,
6};
7
8const BASELINE: (f32, f32, f32) = (200.0, 200.0, 210.0);
10const ACTIVE_CARRIER: (f32, f32, f32) = (255.0, 255.0, 255.0);
13
14const FRENZY_TINT: (f32, f32, f32) = (255.0, 60.0, 60.0);
20const FRENZY_CYCLE: f32 = 13.0;
21const BUFF_TINT: (f32, f32, f32) = (200.0, 100.0, 255.0);
22const BUFF_CYCLE: f32 = 17.0;
23const LUCKY_TINT: (f32, f32, f32) = (255.0, 215.0, 0.0);
24const LUCKY_CYCLE: f32 = 23.0;
25const PURCHASE_TINT: (f32, f32, f32) = (40.0, 230.0, 80.0);
26const PURCHASE_CYCLE: f32 = 11.0;
27const ACHIEVEMENT_TINT: (f32, f32, f32) = (255.0, 200.0, 100.0);
31const ACHIEVEMENT_CYCLE: f32 = 19.0;
32const GREEN_COIN_TINT: (f32, f32, f32) = (120.0, 255.0, 140.0);
36const GREEN_COIN_CYCLE: f32 = 29.0;
37
38const PRESTIGE_TINT: (f32, f32, f32) = (200.0, 170.0, 80.0);
40const PRESTIGE_WEIGHT: f32 = 0.08;
41
42pub fn draw_animated(frame: &mut Frame, area: Rect, state: &GameState, title: &str) {
43 let block = Block::bordered().title(title);
44 frame.render_widget(block, area);
45
46 if area.width < 2 || area.height < 2 {
47 return;
48 }
49
50 let buf = frame.buffer_mut();
51 let mut i: usize = 0;
52 let last_x = area.x + area.width - 1;
53 let last_y = area.y + area.height - 1;
54
55 for x in area.x..=last_x {
56 recolor(buf, x, area.y, cell_color(i, state));
57 i += 1;
58 }
59 for y in (area.y + 1)..=last_y {
60 recolor(buf, last_x, y, cell_color(i, state));
61 i += 1;
62 }
63 if area.height > 1 {
64 for x in (area.x..last_x).rev() {
65 recolor(buf, x, last_y, cell_color(i, state));
66 i += 1;
67 }
68 }
69 if area.width > 1 && area.height > 2 {
70 for y in ((area.y + 1)..last_y).rev() {
71 recolor(buf, area.x, y, cell_color(i, state));
72 i += 1;
73 }
74 }
75}
76
77fn recolor(buf: &mut Buffer, x: u16, y: u16, color: Color) {
78 if x >= buf.area.x + buf.area.width || y >= buf.area.y + buf.area.height {
79 return;
80 }
81 let cell = &mut buf[(x, y)];
82 cell.set_fg(color);
83 cell.modifier.insert(Modifier::BOLD);
84}
85
86fn cell_color(i: usize, state: &GameState) -> Color {
87 let phase = state.border_phase as f32;
88
89 let purchase_s = plateau_fade(state.purchase_flash_ticks, PURCHASE_FLASH_TICKS);
96 let lucky_s = plateau_fade(state.lucky_flash_ticks, LUCKY_FLASH_TICKS);
97 let achievement_s = plateau_fade(state.achievement_flash_ticks, ACHIEVEMENT_FLASH_TICKS);
98 let green_coin_s = plateau_fade(state.green_coin_flash_ticks, GREEN_COIN_FLASH_TICKS);
99 let frenzy_s = state
103 .buffs
104 .iter()
105 .map(|b| {
106 let Buff::ClickFrenzy { .. } = b;
107 b.strength()
108 })
109 .fold(0.0_f32, f32::max);
110 let buff_s = state
113 .fingerers_state
114 .values()
115 .flat_map(|st| st.modifiers.iter())
116 .filter(|m| {
117 matches!(
118 m.duration,
119 crate::game::modifier::ModifierDuration::Ticks(_)
120 )
121 })
122 .map(|m| m.strength())
123 .fold(0.0_f32, f32::max);
124
125 let activity = purchase_s
130 .max(lucky_s)
131 .max(achievement_s)
132 .max(frenzy_s)
133 .max(buff_s)
134 .max(green_coin_s);
135 let carrier_r = BASELINE.0 + (ACTIVE_CARRIER.0 - BASELINE.0) * activity;
136 let carrier_g = BASELINE.1 + (ACTIVE_CARRIER.1 - BASELINE.1) * activity;
137 let carrier_b = BASELINE.2 + (ACTIVE_CARRIER.2 - BASELINE.2) * activity;
138
139 let mut r = carrier_r;
140 let mut g = carrier_g;
141 let mut b = carrier_b;
142
143 let purchase_amp = state.purchase_flash_strength.clamp(1.0, 3.0);
152 for (tint, cycle, strength, amp) in [
153 (PURCHASE_TINT, PURCHASE_CYCLE, purchase_s, purchase_amp),
154 (LUCKY_TINT, LUCKY_CYCLE, lucky_s, 1.0),
155 (ACHIEVEMENT_TINT, ACHIEVEMENT_CYCLE, achievement_s, 1.0),
156 (BUFF_TINT, BUFF_CYCLE, buff_s, 1.0),
157 (FRENZY_TINT, FRENZY_CYCLE, frenzy_s, 1.0),
158 (GREEN_COIN_TINT, GREEN_COIN_CYCLE, green_coin_s, 1.0),
159 ] {
160 if strength > 0.001 {
161 let wave01 = (((i as f32 + phase) * std::f32::consts::TAU / cycle).sin() + 1.0) * 0.5;
162 let contribution = (wave01 * strength * amp).min(1.0);
163 r += (tint.0 - carrier_r) * contribution;
164 g += (tint.1 - carrier_g) * contribution;
165 b += (tint.2 - carrier_b) * contribution;
166 }
167 }
168
169 let mut r = r.clamp(0.0, 255.0);
170 let mut g = g.clamp(0.0, 255.0);
171 let mut b = b.clamp(0.0, 255.0);
172
173 if state.prestige > 0 {
174 r += (PRESTIGE_TINT.0 - r) * PRESTIGE_WEIGHT;
175 g += (PRESTIGE_TINT.1 - g) * PRESTIGE_WEIGHT;
176 b += (PRESTIGE_TINT.2 - b) * PRESTIGE_WEIGHT;
177 }
178
179 Color::Rgb(r as u8, g as u8, b as u8)
180}
181
182fn smoothstep(t: f32) -> f32 {
183 let t = t.clamp(0.0, 1.0);
184 t * t * (3.0 - 2.0 * t)
185}
186
187pub fn plateau_fade(remaining: u32, total: u32) -> f32 {
190 if total == 0 {
191 return 0.0;
192 }
193 let fade_ticks = (total as f32 * 0.4).max(1.0);
194 let r = remaining as f32;
195 if r >= fade_ticks {
196 1.0
197 } else {
198 smoothstep(r / fade_ticks)
199 }
200}
201
202pub fn paint_border_flash(
211 frame: &mut Frame,
212 area: Rect,
213 state: &GameState,
214 tint: (f32, f32, f32),
215 cycle: f32,
216 strength: f32,
217) {
218 if strength <= 0.001 || area.width < 2 || area.height < 2 {
219 return;
220 }
221 let buf = frame.buffer_mut();
222 let phase = state.steady_phase as f32;
227 let last_x = area.x + area.width - 1;
228 let last_y = area.y + area.height - 1;
229
230 let mut paint = |x: u16, y: u16, i: usize| {
231 if x >= buf.area.x + buf.area.width || y >= buf.area.y + buf.area.height {
232 return;
233 }
234 let wave01 = (((i as f32 + phase) * std::f32::consts::TAU / cycle).sin() + 1.0) * 0.5;
235 let carrier_r = BASELINE.0 + (ACTIVE_CARRIER.0 - BASELINE.0) * strength;
238 let carrier_g = BASELINE.1 + (ACTIVE_CARRIER.1 - BASELINE.1) * strength;
239 let carrier_b = BASELINE.2 + (ACTIVE_CARRIER.2 - BASELINE.2) * strength;
240 let contribution = wave01 * strength;
241 let r = carrier_r + (tint.0 - carrier_r) * contribution;
242 let g = carrier_g + (tint.1 - carrier_g) * contribution;
243 let b = carrier_b + (tint.2 - carrier_b) * contribution;
244 let cell = &mut buf[(x, y)];
245 cell.set_fg(Color::Rgb(
246 r.clamp(0.0, 255.0) as u8,
247 g.clamp(0.0, 255.0) as u8,
248 b.clamp(0.0, 255.0) as u8,
249 ));
250 cell.modifier.insert(Modifier::BOLD);
251 };
252
253 let mut i = 0usize;
254 for x in area.x..=last_x {
255 paint(x, area.y, i);
256 i += 1;
257 }
258 for y in (area.y + 1)..=last_y {
259 paint(last_x, y, i);
260 i += 1;
261 }
262 if area.height > 1 {
263 for x in (area.x..last_x).rev() {
264 paint(x, last_y, i);
265 i += 1;
266 }
267 }
268 if area.width > 1 && area.height > 2 {
269 for y in ((area.y + 1)..last_y).rev() {
270 paint(area.x, y, i);
271 i += 1;
272 }
273 }
274}
275
276pub const PANEL_PURCHASE_TINT: (f32, f32, f32) = PURCHASE_TINT;
281pub const PANEL_PURCHASE_CYCLE: f32 = PURCHASE_CYCLE;
282pub const PANEL_UNAFFORDABLE_TINT: (f32, f32, f32) = (255.0, 70.0, 70.0);
283pub const PANEL_UNAFFORDABLE_CYCLE: f32 = 7.0;
284pub const PANEL_ACHIEVEMENT_TINT: (f32, f32, f32) = ACHIEVEMENT_TINT;
285pub const PANEL_ACHIEVEMENT_CYCLE: f32 = ACHIEVEMENT_CYCLE;