1use ratatui::{prelude::*, widgets::*};
2
3use crate::format;
4use crate::game::state::{GameState, PURCHASE_FLASH_TICKS, UNLOCK_FLASH_TICKS};
5use crate::game::upgrade::{self, UPGRADES};
6use crate::i18n::t;
7use crate::ui::border;
8
9const HANGING_INDENT: &str = " ";
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);
15const FLASH_REST: (f32, f32, f32) = (200.0, 200.0, 210.0);
16const FLASH_CARRIER: (f32, f32, f32) = (255.0, 255.0, 255.0);
17const FLASH_CYCLE: f32 = 11.0;
18
19pub fn draw(
24 frame: &mut Frame,
25 area: Rect,
26 state: &GameState,
27 mouse_pos: Option<(u16, u16)>,
28) -> Vec<(usize, Rect)> {
29 let lang = t();
30 let available = upgrade::available_ids(state);
31 let visible: Vec<usize> = available.iter().take(10).copied().collect();
32 let desc_width = area.width.saturating_sub(2 + HANGING_INDENT.len() as u16) as usize;
36
37 let mut lines: Vec<Line> = Vec::new();
38 let mut row_spans: Vec<UpgradeRowSpan> = Vec::new();
46
47 if visible.is_empty() {
48 for l in lang.upgrades_none.lines() {
49 lines.push(Line::from(l.to_string()).style(Style::default().fg(Color::DarkGray)));
50 }
51 } else {
52 for (slot, &u_idx) in visible.iter().enumerate() {
53 let hotkey = if slot == 9 {
54 '0'
55 } else {
56 (b'1' + slot as u8) as char
57 };
58 let u = &UPGRADES[u_idx];
59 let affordable = state.affordable_cuques() >= u.cost;
62 let name = lang.upgrade_names.get(u_idx).copied().unwrap_or("?");
63 let desc = lang.upgrade_descs.get(u_idx).copied().unwrap_or("");
64 let cost_style = if affordable {
65 Style::default()
66 .fg(Color::Rgb(0, 255, 80))
67 .add_modifier(Modifier::BOLD)
68 } else {
69 Style::default().fg(Color::Rgb(220, 70, 70))
70 };
71 let content_start = lines.len() as u16;
73 lines.push(Line::from(vec![
74 Span::styled(format!("[{}] ", hotkey), Style::default().fg(Color::Yellow)),
75 Span::styled(
76 name.to_string(),
77 Style::default().add_modifier(Modifier::BOLD),
78 ),
79 ]));
80 let wrapped = wrap_hanging(desc, desc_width);
84 for desc_line in &wrapped {
85 lines.push(Line::from(vec![Span::styled(
86 desc_line.clone(),
87 Style::default().fg(Color::DarkGray),
88 )]));
89 }
90 lines.push(Line::from(vec![
91 Span::raw(HANGING_INDENT),
92 Span::styled(format!("{} {}", lang.cost, format::big(u.cost)), cost_style),
93 ]));
94 let content_height = (1 + wrapped.len() as u16 + 1).max(1);
96 row_spans.push(UpgradeRowSpan {
97 u_idx,
98 content_start,
99 content_height,
100 });
101 lines.push(Line::raw("")); }
103 }
104
105 let p = Paragraph::new(lines).block(Block::bordered().title(lang.upgrades_title));
106 frame.render_widget(p, area);
107
108 paint_flashes(frame, area, state, &row_spans);
109
110 let any_purchase = visible
113 .iter()
114 .filter_map(|&i| state.upgrade_flash_ticks.get(i).copied())
115 .max()
116 .unwrap_or(0);
117 let any_unaff = visible
118 .iter()
119 .filter_map(|&i| state.upgrade_unaffordable_flash.get(i).copied())
120 .max()
121 .unwrap_or(0);
122 let purchase_strength = border::plateau_fade(any_purchase, PURCHASE_FLASH_TICKS);
126 let unaff_strength = border::plateau_fade(any_unaff, PURCHASE_FLASH_TICKS / 2);
127 if unaff_strength > 0.001 {
132 border::paint_border_flash(
133 frame,
134 area,
135 state,
136 border::PANEL_UNAFFORDABLE_TINT,
137 border::PANEL_UNAFFORDABLE_CYCLE,
138 unaff_strength,
139 );
140 } else if purchase_strength > 0.001 {
141 border::paint_border_flash(
142 frame,
143 area,
144 state,
145 border::PANEL_PURCHASE_TINT,
146 border::PANEL_PURCHASE_CYCLE,
147 purchase_strength,
148 );
149 }
150
151 if area.width < 3 || area.height < 3 {
156 return Vec::new();
157 }
158 let inner_x = area.x + 1;
159 let inner_y = area.y + 1;
160 let inner_w = area.width.saturating_sub(2);
161 let inner_h = area.height.saturating_sub(2);
162 let mut rows: Vec<(usize, Rect)> = Vec::new();
163 for span in &row_spans {
164 if span.content_start >= inner_h {
165 break;
166 }
167 let height = span.content_height.min(inner_h - span.content_start);
168 rows.push((
169 span.u_idx,
170 Rect {
171 x: inner_x,
172 y: inner_y + span.content_start,
173 width: inner_w,
174 height,
175 },
176 ));
177 }
178 paint_hover(frame, &rows, mouse_pos);
179 rows
180}
181
182struct UpgradeRowSpan {
188 u_idx: usize,
189 content_start: u16,
190 content_height: u16,
191}
192
193fn paint_hover(frame: &mut Frame, rows: &[(usize, Rect)], mouse_pos: Option<(u16, u16)>) {
195 let Some((mx, my)) = mouse_pos else { return };
196 let Some(&(_, r)) = rows
197 .iter()
198 .find(|&&(_, r)| mx >= r.x && mx < r.x + r.width && my >= r.y && my < r.y + r.height)
199 else {
200 return;
201 };
202 let buf = frame.buffer_mut();
203 for dy in 0..r.height {
204 let y = r.y + dy;
205 if y >= buf.area.y + buf.area.height {
206 break;
207 }
208 for dx in 0..r.width {
209 let x = r.x + dx;
210 if x >= buf.area.x + buf.area.width {
211 break;
212 }
213 let cell = &mut buf[(x, y)];
214 if let Color::Rgb(r, g, b) = cell.fg {
215 cell.set_fg(Color::Rgb(
216 (r as u16 + 30).min(255) as u8,
217 (g as u16 + 30).min(255) as u8,
218 (b as u16 + 30).min(255) as u8,
219 ));
220 }
221 cell.modifier.insert(Modifier::BOLD);
222 cell.set_bg(Color::Rgb(28, 28, 36));
223 }
224 }
225}
226
227fn wrap_hanging(text: &str, width: usize) -> Vec<String> {
231 let indent = HANGING_INDENT;
232 if width <= indent.len() {
233 return vec![format!("{indent}{text}")];
234 }
235 let avail = width - indent.len();
236 let mut out: Vec<String> = Vec::new();
237 let mut row = String::new();
238 for word in text.split_whitespace() {
239 if row.is_empty() {
240 row.push_str(word);
241 } else if row.len() + 1 + word.len() <= avail {
242 row.push(' ');
243 row.push_str(word);
244 } else {
245 out.push(format!("{indent}{row}"));
246 row.clear();
247 row.push_str(word);
248 }
249 }
250 if !row.is_empty() || out.is_empty() {
251 out.push(format!("{indent}{row}"));
252 }
253 out
254}
255
256fn paint_flashes(frame: &mut Frame, area: Rect, state: &GameState, row_spans: &[UpgradeRowSpan]) {
257 if area.width < 3 || area.height < 3 {
258 return;
259 }
260 let phase = state.steady_phase as f32;
263 let inner_x = area.x + 1;
264 let inner_y = area.y + 1;
265 let inner_right = area.x + area.width - 1;
266 let inner_bottom = area.y + area.height - 1;
267 let bulk_amp = state.purchase_flash_strength.clamp(1.0, 3.0);
270 let buf = frame.buffer_mut();
271
272 for span in row_spans {
273 let purchase_ticks = state
274 .upgrade_flash_ticks
275 .get(span.u_idx)
276 .copied()
277 .unwrap_or(0);
278 let unaff_ticks = state
279 .upgrade_unaffordable_flash
280 .get(span.u_idx)
281 .copied()
282 .unwrap_or(0);
283 let unlock_ticks = state
284 .upgrade_unlock_flash
285 .get(span.u_idx)
286 .copied()
287 .unwrap_or(0);
288 let (strength, tint, amp) = if purchase_ticks > 0 {
289 (
290 smoothstep(purchase_ticks as f32 / PURCHASE_FLASH_TICKS as f32),
291 FLASH_TINT,
292 bulk_amp,
293 )
294 } else if unaff_ticks > 0 {
295 (
296 smoothstep(unaff_ticks as f32 / (PURCHASE_FLASH_TICKS as f32 / 2.0)),
297 UNAFFORDABLE_TINT,
298 1.0,
299 )
300 } else if unlock_ticks > 0 {
301 (
302 smoothstep(unlock_ticks as f32 / UNLOCK_FLASH_TICKS as f32),
303 UNLOCK_TINT,
304 1.0,
305 )
306 } else {
307 continue;
308 };
309 if strength <= 0.001 {
310 continue;
311 }
312 let row_start = inner_y + span.content_start;
313 let carrier_r = FLASH_REST.0 + (FLASH_CARRIER.0 - FLASH_REST.0) * strength;
314 let carrier_g = FLASH_REST.1 + (FLASH_CARRIER.1 - FLASH_REST.1) * strength;
315 let carrier_b = FLASH_REST.2 + (FLASH_CARRIER.2 - FLASH_REST.2) * strength;
316 for dy in 0..span.content_height {
317 let row = row_start + dy;
318 if row >= inner_bottom {
319 break;
320 }
321 for col in inner_x..inner_right {
322 let rel = (col - area.x) as f32;
323 let wave01 =
324 (((rel + phase) * std::f32::consts::TAU / FLASH_CYCLE).sin() + 1.0) * 0.5;
325 let contribution = (wave01 * strength * amp).min(1.0);
326 let r = carrier_r + (tint.0 - carrier_r) * contribution;
327 let g = carrier_g + (tint.1 - carrier_g) * contribution;
328 let b = carrier_b + (tint.2 - carrier_b) * contribution;
329 let cell = &mut buf[(col, row)];
330 cell.set_fg(Color::Rgb(r as u8, g as u8, b as u8));
331 cell.modifier.insert(Modifier::BOLD);
332 }
333 }
334 }
335}
336
337fn smoothstep(t: f32) -> f32 {
338 let t = t.clamp(0.0, 1.0);
339 t * t * (3.0 - 2.0 * t)
340}