1use ratatui::{prelude::*, widgets::*};
2
3use crate::format;
4use crate::game::fingerer::{self, FINGERERS};
5use crate::game::state::{GameState, PURCHASE_FLASH_TICKS, UNLOCK_FLASH_TICKS};
6use crate::i18n::t;
7use crate::ui::border;
8
9const ROWS_PER_FINGERER: u16 = 4;
10const FLASH_TINT: (f32, f32, f32) = (40.0, 230.0, 80.0);
11const UNAFFORDABLE_TINT: (f32, f32, f32) = (255.0, 60.0, 60.0);
12const UNLOCK_TINT: (f32, f32, f32) = (120.0, 255.0, 140.0);
16const FLASH_REST: (f32, f32, f32) = (200.0, 200.0, 210.0);
19const FLASH_CARRIER: (f32, f32, f32) = (255.0, 255.0, 255.0);
22const FLASH_CYCLE: f32 = 11.0;
23
24pub fn draw(
29 frame: &mut Frame,
30 area: Rect,
31 state: &GameState,
32 mouse_pos: Option<(u16, u16)>,
33) -> Vec<(usize, Rect)> {
34 let lang = t();
35 let mut lines: Vec<Line> = Vec::new();
36 let visible: Vec<usize> = (0..FINGERERS.len())
37 .filter(|&i| fingerer::visible(i, state.fingerer_count_idx(i), state.lifetime_cuques))
38 .collect();
39
40 for (slot, &i) in visible.iter().enumerate() {
41 if slot >= 10 {
42 break;
43 }
44 let hotkey = if slot == 9 {
45 '0'
46 } else {
47 (b'1' + slot as u8) as char
48 };
49 let k = &FINGERERS[i];
50 let owned = state.fingerer_count_idx(i);
51 let cost = state.cost(i);
52 let affordable = state.can_buy(i);
53 let cost_style = if affordable {
54 Style::default()
55 .fg(Color::Rgb(0, 255, 80))
56 .add_modifier(Modifier::BOLD)
57 } else {
58 Style::default().fg(Color::Rgb(220, 70, 70))
59 };
60 let name = lang.fingerer_names.get(i).copied().unwrap_or("?");
61 lines.push(Line::from(vec![
62 Span::styled(format!("[{}] ", hotkey), Style::default().fg(Color::Yellow)),
63 Span::raw(k.icon),
64 Span::raw(" "),
65 Span::styled(
66 name.to_string(),
67 Style::default().add_modifier(Modifier::BOLD),
68 ),
69 ]));
70 lines.push(Line::from(vec![
71 Span::raw(format!(" {}: {} ", lang.owned, owned)),
72 Span::styled(format!("{} {}", lang.cost, format::big(cost)), cost_style),
73 ]));
74 let mult = state.fingerer_mult(i);
75 let effective = k.fps_per_unit * mult;
76 let mult_tag = if mult > 1.0001 {
77 format!(" (x{:.1})", mult)
78 } else {
79 String::new()
80 };
81 let perm_pct: f64 = state
87 .fingerers_state
88 .get(k.id)
89 .map(|st| {
90 st.modifiers
91 .iter()
92 .filter(|m| {
93 matches!(
94 m.duration,
95 crate::game::modifier::ModifierDuration::Permanent
96 )
97 })
98 .flat_map(|m| m.effects.iter())
99 .filter_map(|e| match e {
100 crate::game::modifier::ModifierEffect::AddPercent(v) => Some(*v),
101 _ => None,
102 })
103 .sum()
104 })
105 .unwrap_or(0.0);
106 let mut spans = vec![Span::raw(format!(
107 " +{} {}{}",
108 format::rate(effective),
109 lang.fps_each,
110 mult_tag,
111 ))];
112 if perm_pct > 0.0001 {
113 spans.push(Span::styled(
114 format!(" +{:.0}%", perm_pct * 100.0),
115 Style::default()
116 .fg(Color::Rgb(120, 230, 140))
117 .add_modifier(Modifier::BOLD),
118 ));
119 }
120 lines.push(Line::from(spans));
121 lines.push(Line::raw(""));
122 }
123 let p = Paragraph::new(lines).block(Block::bordered().title(lang.fingerers_title));
124 frame.render_widget(p, area);
125
126 paint_flashes(frame, area, state, &visible);
127
128 let any_purchase = visible
132 .iter()
133 .filter_map(|&i| state.fingerer_flash_ticks.get(i).copied())
134 .max()
135 .unwrap_or(0);
136 let any_unaff = visible
137 .iter()
138 .filter_map(|&i| state.fingerer_unaffordable_flash.get(i).copied())
139 .max()
140 .unwrap_or(0);
141 let purchase_strength = border::plateau_fade(any_purchase, PURCHASE_FLASH_TICKS);
145 let unaff_strength = border::plateau_fade(any_unaff, PURCHASE_FLASH_TICKS / 2);
146 if unaff_strength > 0.001 {
152 border::paint_border_flash(
153 frame,
154 area,
155 state,
156 border::PANEL_UNAFFORDABLE_TINT,
157 border::PANEL_UNAFFORDABLE_CYCLE,
158 unaff_strength,
159 );
160 } else if purchase_strength > 0.001 {
161 border::paint_border_flash(
162 frame,
163 area,
164 state,
165 border::PANEL_PURCHASE_TINT,
166 border::PANEL_PURCHASE_CYCLE,
167 purchase_strength,
168 );
169 }
170
171 if area.width < 3 || area.height < 3 {
172 return Vec::new();
173 }
174 let inner_x = area.x + 1;
175 let inner_y = area.y + 1;
176 let inner_w = area.width.saturating_sub(2);
177 let inner_h = area.height.saturating_sub(2);
178 let mut rows: Vec<(usize, Rect)> = Vec::new();
179 for (slot, &i) in visible.iter().enumerate() {
180 if slot >= 10 {
181 break;
182 }
183 let row_top = slot as u16 * ROWS_PER_FINGERER;
184 if row_top >= inner_h {
185 break;
186 }
187 let height = (ROWS_PER_FINGERER - 1).min(inner_h - row_top);
189 rows.push((
190 i,
191 Rect {
192 x: inner_x,
193 y: inner_y + row_top,
194 width: inner_w,
195 height,
196 },
197 ));
198 }
199 paint_hover(frame, &rows, mouse_pos);
200 rows
201}
202
203fn paint_hover(frame: &mut Frame, rows: &[(usize, Rect)], mouse_pos: Option<(u16, u16)>) {
209 let Some((mx, my)) = mouse_pos else { return };
210 let Some(&(_, r)) = rows
211 .iter()
212 .find(|&&(_, r)| mx >= r.x && mx < r.x + r.width && my >= r.y && my < r.y + r.height)
213 else {
214 return;
215 };
216 let buf = frame.buffer_mut();
217 for dy in 0..r.height {
218 let y = r.y + dy;
219 if y >= buf.area.y + buf.area.height {
220 break;
221 }
222 for dx in 0..r.width {
223 let x = r.x + dx;
224 if x >= buf.area.x + buf.area.width {
225 break;
226 }
227 let cell = &mut buf[(x, y)];
228 if let Color::Rgb(r, g, b) = cell.fg {
232 cell.set_fg(Color::Rgb(
233 (r as u16 + 30).min(255) as u8,
234 (g as u16 + 30).min(255) as u8,
235 (b as u16 + 30).min(255) as u8,
236 ));
237 }
238 cell.modifier.insert(Modifier::BOLD);
239 cell.set_bg(Color::Rgb(28, 28, 36));
241 }
242 }
243}
244
245fn paint_flashes(frame: &mut Frame, area: Rect, state: &GameState, visible: &[usize]) {
246 if area.width < 3 || area.height < 3 {
247 return;
248 }
249 let phase = state.steady_phase as f32;
253 let inner_x = area.x + 1;
254 let inner_y = area.y + 1;
255 let inner_right = area.x + area.width - 1;
256 let inner_bottom = area.y + area.height - 1;
257 let bulk_amp = state.purchase_flash_strength.clamp(1.0, 3.0);
262 let buf = frame.buffer_mut();
263
264 for (slot, &fingerer_idx) in visible.iter().enumerate() {
265 if slot >= 10 {
266 break;
267 }
268 let purchase_ticks = state
269 .fingerer_flash_ticks
270 .get(fingerer_idx)
271 .copied()
272 .unwrap_or(0);
273 let unaff_ticks = state
274 .fingerer_unaffordable_flash
275 .get(fingerer_idx)
276 .copied()
277 .unwrap_or(0);
278 let unlock_ticks = state
279 .fingerer_unlock_flash
280 .get(fingerer_idx)
281 .copied()
282 .unwrap_or(0);
283 let (strength, tint, amp) = if purchase_ticks > 0 {
290 (
291 smoothstep(purchase_ticks as f32 / PURCHASE_FLASH_TICKS as f32),
292 FLASH_TINT,
293 bulk_amp,
294 )
295 } else if unaff_ticks > 0 {
296 (
297 smoothstep(unaff_ticks as f32 / (PURCHASE_FLASH_TICKS as f32 / 2.0)),
298 UNAFFORDABLE_TINT,
299 1.0,
300 )
301 } else if unlock_ticks > 0 {
302 (
303 smoothstep(unlock_ticks as f32 / UNLOCK_FLASH_TICKS as f32),
304 UNLOCK_TINT,
305 1.0,
306 )
307 } else {
308 continue;
309 };
310 if strength <= 0.001 {
311 continue;
312 }
313 let row_start = inner_y + slot as u16 * ROWS_PER_FINGERER;
314 let carrier_r = FLASH_REST.0 + (FLASH_CARRIER.0 - FLASH_REST.0) * strength;
317 let carrier_g = FLASH_REST.1 + (FLASH_CARRIER.1 - FLASH_REST.1) * strength;
318 let carrier_b = FLASH_REST.2 + (FLASH_CARRIER.2 - FLASH_REST.2) * strength;
319 for dy in 0..3u16 {
322 let row = row_start + dy;
323 if row >= inner_bottom {
324 break;
325 }
326 for col in inner_x..inner_right {
327 let rel = (col - area.x) as f32;
328 let wave01 =
329 (((rel + phase) * std::f32::consts::TAU / FLASH_CYCLE).sin() + 1.0) * 0.5;
330 let contribution = (wave01 * strength * amp).min(1.0);
331 let r = carrier_r + (tint.0 - carrier_r) * contribution;
332 let g = carrier_g + (tint.1 - carrier_g) * contribution;
333 let b = carrier_b + (tint.2 - carrier_b) * contribution;
334 let cell = &mut buf[(col, row)];
335 cell.set_fg(Color::Rgb(r as u8, g as u8, b as u8));
336 cell.modifier.insert(Modifier::BOLD);
337 }
338 }
339 }
340}
341
342fn smoothstep(t: f32) -> f32 {
343 let t = t.clamp(0.0, 1.0);
344 t * t * (3.0 - 2.0 * t)
345}