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