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| {
44 fingerer::visible(
45 i,
46 state.fingerer_count_idx(i),
47 state.lifetime_cuques.to_f64(),
48 )
49 })
50 .collect();
51
52 for (slot, &i) in visible.iter().enumerate() {
53 if slot >= 10 {
54 break;
55 }
56 let hotkey = if slot == 9 {
57 '0'
58 } else {
59 (b'1' + slot as u8) as char
60 };
61 let k = &FINGERERS[i];
62 let owned = state.fingerer_count_idx(i);
63 let cost = state.cost(i);
64 let affordable = state.can_buy(i);
65 let cost_style = if affordable {
66 Style::default()
67 .fg(Color::Rgb(0, 255, 80))
68 .add_modifier(Modifier::BOLD)
69 } else {
70 Style::default().fg(Color::Rgb(220, 70, 70))
71 };
72 let name = lang.fingerer_names.get(i).copied().unwrap_or("?");
73 lines.push(Line::from(vec![
74 Span::styled(format!("[{}] ", hotkey), Style::default().fg(Color::Yellow)),
75 Span::raw(k.icon),
76 Span::raw(" "),
77 Span::styled(
78 name.to_string(),
79 Style::default().add_modifier(Modifier::BOLD),
80 ),
81 ]));
82 lines.push(Line::from(vec![
83 Span::raw(format!(" {}: {} ", lang.owned, owned)),
84 Span::styled(
85 format!("{} {}", lang.cost, format::big_mag(cost)),
86 cost_style,
87 ),
88 ]));
89 let tree_contrib = state.tree_aggregate.effective_for_fingerer(i);
94 let mult = tree_contrib.mul_factor;
95 let mult_f = mult.to_f64();
102 let effective = k.fps_per_unit * mult_f;
103 let mult_tag = if mult_f > 1.0001 {
104 format!(" (x{:.1})", mult_f)
105 } else {
106 String::new()
107 };
108 let perm_pct: f64 = state
114 .fingerers_state
115 .get(k.id)
116 .map(|st| {
117 st.modifiers
118 .iter()
119 .filter(|m| {
120 matches!(
121 m.duration,
122 crate::game::modifier::ModifierDuration::Permanent
123 )
124 })
125 .flat_map(|m| m.effects.iter())
126 .filter_map(|e| match e {
127 crate::game::modifier::ModifierEffect::AddPercent(v) => Some(*v),
128 _ => None,
129 })
130 .sum()
131 })
132 .unwrap_or(0.0);
133 let mut spans = vec![Span::raw(format!(
134 " +{} {}{}",
135 format::rate(effective),
136 lang.fps_each,
137 mult_tag,
138 ))];
139 if perm_pct > 0.0001 {
140 spans.push(Span::styled(
141 format!(" +{:.0}%", perm_pct * 100.0),
142 Style::default()
143 .fg(Color::Rgb(120, 230, 140))
144 .add_modifier(Modifier::BOLD),
145 ));
146 }
147 lines.push(Line::from(spans));
148 lines.push(Line::raw(""));
149 }
150 let p = Paragraph::new(lines).block(Block::bordered().title(lang.fingerers_title));
151 frame.render_widget(p, area);
152
153 paint_flashes(frame, area, state, &visible);
154
155 let any_purchase = visible
159 .iter()
160 .filter_map(|&i| state.fingerer_flash_ticks.get(i).copied())
161 .max()
162 .unwrap_or(0);
163 let any_unaff = visible
164 .iter()
165 .filter_map(|&i| state.fingerer_unaffordable_flash.get(i).copied())
166 .max()
167 .unwrap_or(0);
168 let purchase_strength = border::plateau_fade(any_purchase, PURCHASE_FLASH_TICKS);
172 let unaff_strength = border::plateau_fade(any_unaff, PURCHASE_FLASH_TICKS / 2);
173 if unaff_strength > 0.001 {
179 border::paint_border_flash(
180 frame,
181 area,
182 state,
183 border::PANEL_UNAFFORDABLE_TINT,
184 border::PANEL_UNAFFORDABLE_CYCLE,
185 unaff_strength,
186 );
187 } else if purchase_strength > 0.001 {
188 border::paint_border_flash(
189 frame,
190 area,
191 state,
192 border::PANEL_PURCHASE_TINT,
193 border::PANEL_PURCHASE_CYCLE,
194 purchase_strength,
195 );
196 }
197
198 if area.width < 3 || area.height < 3 {
199 return Vec::new();
200 }
201 let inner_x = area.x + 1;
202 let inner_y = area.y + 1;
203 let inner_w = area.width.saturating_sub(2);
204 let inner_h = area.height.saturating_sub(2);
205 let mut rows: Vec<(usize, Rect)> = Vec::new();
206 for (slot, &i) in visible.iter().enumerate() {
207 if slot >= 10 {
208 break;
209 }
210 let row_top = slot as u16 * ROWS_PER_FINGERER;
211 if row_top >= inner_h {
212 break;
213 }
214 let height = (ROWS_PER_FINGERER - 1).min(inner_h - row_top);
216 rows.push((
217 i,
218 Rect {
219 x: inner_x,
220 y: inner_y + row_top,
221 width: inner_w,
222 height,
223 },
224 ));
225 }
226 paint_hover(frame, &rows, mouse_pos);
227 rows
228}
229
230fn paint_hover(frame: &mut Frame, rows: &[(usize, Rect)], mouse_pos: Option<(u16, u16)>) {
236 let Some((mx, my)) = mouse_pos else { return };
237 let Some(&(_, r)) = rows
238 .iter()
239 .find(|&&(_, r)| mx >= r.x && mx < r.x + r.width && my >= r.y && my < r.y + r.height)
240 else {
241 return;
242 };
243 let buf = frame.buffer_mut();
244 for dy in 0..r.height {
245 let y = r.y + dy;
246 if y >= buf.area.y + buf.area.height {
247 break;
248 }
249 for dx in 0..r.width {
250 let x = r.x + dx;
251 if x >= buf.area.x + buf.area.width {
252 break;
253 }
254 let cell = &mut buf[(x, y)];
255 if let Color::Rgb(r, g, b) = cell.fg {
259 cell.set_fg(Color::Rgb(
260 (r as u16 + 30).min(255) as u8,
261 (g as u16 + 30).min(255) as u8,
262 (b as u16 + 30).min(255) as u8,
263 ));
264 }
265 cell.modifier.insert(Modifier::BOLD);
266 cell.set_bg(Color::Rgb(28, 28, 36));
268 }
269 }
270}
271
272fn paint_flashes(frame: &mut Frame, area: Rect, state: &GameState, visible: &[usize]) {
273 if area.width < 3 || area.height < 3 {
274 return;
275 }
276 let phase = state.steady_phase as f32;
280 let inner_x = area.x + 1;
281 let inner_y = area.y + 1;
282 let inner_right = area.x + area.width - 1;
283 let inner_bottom = area.y + area.height - 1;
284 let bulk_amp = state.purchase_flash_strength.clamp(1.0, 3.0);
289 let buf = frame.buffer_mut();
290
291 for (slot, &fingerer_idx) in visible.iter().enumerate() {
292 if slot >= 10 {
293 break;
294 }
295 let purchase_ticks = state
296 .fingerer_flash_ticks
297 .get(fingerer_idx)
298 .copied()
299 .unwrap_or(0);
300 let unaff_ticks = state
301 .fingerer_unaffordable_flash
302 .get(fingerer_idx)
303 .copied()
304 .unwrap_or(0);
305 let unlock_ticks = state
306 .fingerer_unlock_flash
307 .get(fingerer_idx)
308 .copied()
309 .unwrap_or(0);
310 let green_coin_ticks = state
311 .fingerer_green_coin_flash
312 .get(fingerer_idx)
313 .copied()
314 .unwrap_or(0);
315 let (strength, tint, amp) = if purchase_ticks > 0 {
323 (
324 smoothstep(purchase_ticks as f32 / PURCHASE_FLASH_TICKS as f32),
325 FLASH_TINT,
326 bulk_amp,
327 )
328 } else if unaff_ticks > 0 {
329 (
330 smoothstep(unaff_ticks as f32 / (PURCHASE_FLASH_TICKS as f32 / 2.0)),
331 UNAFFORDABLE_TINT,
332 1.0,
333 )
334 } else if green_coin_ticks > 0 {
335 (
336 smoothstep(green_coin_ticks as f32 / GREEN_COIN_ROW_FLASH_TICKS as f32),
337 GREEN_COIN_ROW_TINT,
338 1.0,
339 )
340 } else if unlock_ticks > 0 {
341 (
342 smoothstep(unlock_ticks as f32 / UNLOCK_FLASH_TICKS as f32),
343 UNLOCK_TINT,
344 1.0,
345 )
346 } else {
347 continue;
348 };
349 if strength <= 0.001 {
350 continue;
351 }
352 let row_start = inner_y + slot as u16 * ROWS_PER_FINGERER;
353 let carrier_r = FLASH_REST.0 + (FLASH_CARRIER.0 - FLASH_REST.0) * strength;
356 let carrier_g = FLASH_REST.1 + (FLASH_CARRIER.1 - FLASH_REST.1) * strength;
357 let carrier_b = FLASH_REST.2 + (FLASH_CARRIER.2 - FLASH_REST.2) * strength;
358 for dy in 0..3u16 {
361 let row = row_start + dy;
362 if row >= inner_bottom {
363 break;
364 }
365 for col in inner_x..inner_right {
366 let rel = (col - area.x) as f32;
367 let wave01 =
368 (((rel + phase) * std::f32::consts::TAU / FLASH_CYCLE).sin() + 1.0) * 0.5;
369 let contribution = (wave01 * strength * amp).min(1.0);
370 let r = carrier_r + (tint.0 - carrier_r) * contribution;
371 let g = carrier_g + (tint.1 - carrier_g) * contribution;
372 let b = carrier_b + (tint.2 - carrier_b) * contribution;
373 let cell = &mut buf[(col, row)];
374 cell.set_fg(Color::Rgb(r as u8, g as u8, b as u8));
375 cell.modifier.insert(Modifier::BOLD);
376 }
377 }
378 }
379}
380
381fn smoothstep(t: f32) -> f32 {
382 let t = t.clamp(0.0, 1.0);
383 t * t * (3.0 - 2.0 * t)
384}