cuqueclicker_lib/ui/
hands.rs1use ratatui::prelude::*;
2
3use crate::game::fingerer::FINGERERS;
4use crate::game::state::GameState;
5
6const PER_TYPE_CAP: usize = 40;
7const PER_RING: usize = 48;
8
9pub fn occupied_at(col: u16, row: u16, biscuit: Rect, state: &GameState) -> bool {
19 if biscuit.width == 0 || biscuit.height == 0 {
20 return false;
21 }
22 let cx = biscuit.x as f32 + biscuit.width as f32 / 2.0;
23 let cy = biscuit.y as f32 + biscuit.height as f32 / 2.0;
24 let base_rx = (biscuit.width as f32 / 2.0 + 3.0).max(6.0);
25 let base_ry = (biscuit.height as f32 / 2.0 + 2.0).max(3.0);
26
27 let mut glyphs: Vec<(usize, &str)> = Vec::new();
28 for (idx, f) in FINGERERS.iter().enumerate() {
29 let n = (state.fingerer_count(f.id) as usize).min(PER_TYPE_CAP);
30 for _ in 0..n {
31 glyphs.push((idx, f.icon));
32 }
33 }
34 if glyphs.is_empty() {
35 return false;
36 }
37
38 let total = glyphs.len();
39 let bx = biscuit.x as i32;
40 let br = bx + biscuit.width as i32;
41 let by = biscuit.y as i32;
42 let bb = by + biscuit.height as i32;
43
44 for (i, (type_idx, icon)) in glyphs.iter().enumerate() {
45 let ring = i / PER_RING;
46 let slot = i % PER_RING;
47 let slot_count = ((total - ring * PER_RING).min(PER_RING)) as f32;
48 let angle = (slot as f32 / slot_count) * std::f32::consts::TAU
49 + (ring as f32 * 0.15)
50 + (*type_idx as f32 * 0.07);
51 let speed = FINGERERS
56 .get(*type_idx)
57 .map(|f| f.poke_speed.max(0.1))
58 .unwrap_or(1.0);
59 let tier_divisor = (5.0 / speed).max(1.0) as u64;
60 let tier_phase = state.session_ticks / tier_divisor;
61 let poke = if ((i * 7) as u64 + tier_phase).is_multiple_of(23) {
62 1.2
63 } else {
64 0.0
65 };
66 let rx = base_rx + ring as f32 * 4.0 - poke;
67 let ry = base_ry + ring as f32 * 2.0 - poke * 0.5;
68 let px = cx + rx * angle.cos();
69 let py = cy + ry * angle.sin();
70 let g_col = px.round() as i32;
71 let g_row = py.round() as i32;
72
73 let icon_w = icon.chars().count() as i32;
74 if g_row >= by && g_row < bb && g_col < br && g_col + icon_w > bx {
77 continue;
78 }
79 if (row as i32) == g_row && (col as i32) >= g_col && (col as i32) < g_col + icon_w {
81 return true;
82 }
83 }
84 false
85}
86
87pub fn draw(frame: &mut Frame, play_area: Rect, biscuit: Rect, state: &GameState) {
88 if play_area.width == 0 || play_area.height == 0 {
89 return;
90 }
91 let buf = frame.buffer_mut();
92 let cx = biscuit.x as f32 + biscuit.width as f32 / 2.0;
93 let cy = biscuit.y as f32 + biscuit.height as f32 / 2.0;
94
95 let base_rx = (biscuit.width as f32 / 2.0 + 3.0).max(6.0);
96 let base_ry = (biscuit.height as f32 / 2.0 + 2.0).max(3.0);
97
98 let mut glyphs: Vec<(usize, &str)> = Vec::new();
99 for (idx, f) in FINGERERS.iter().enumerate() {
100 let n = (state.fingerer_count(f.id) as usize).min(PER_TYPE_CAP);
101 for _ in 0..n {
102 glyphs.push((idx, f.icon));
103 }
104 }
105 if glyphs.is_empty() {
106 return;
107 }
108
109 let total = glyphs.len();
110
111 for (i, (type_idx, icon)) in glyphs.iter().enumerate() {
112 let ring = i / PER_RING;
113 let slot = i % PER_RING;
114 let slot_count = ((total - ring * PER_RING).min(PER_RING)) as f32;
115 let angle = (slot as f32 / slot_count) * std::f32::consts::TAU
116 + (ring as f32 * 0.15)
117 + (*type_idx as f32 * 0.07);
118 let speed = FINGERERS
126 .get(*type_idx)
127 .map(|f| f.poke_speed.max(0.1))
128 .unwrap_or(1.0);
129 let tier_divisor = (5.0 / speed).max(1.0) as u64;
130 let tier_phase = state.session_ticks / tier_divisor;
131 let poke = if ((i * 7) as u64 + tier_phase).is_multiple_of(23) {
132 1.2
133 } else {
134 0.0
135 };
136 let rx = base_rx + ring as f32 * 4.0 - poke;
137 let ry = base_ry + ring as f32 * 2.0 - poke * 0.5;
138 let px = cx + rx * angle.cos();
139 let py = cy + ry * angle.sin();
140 let col = px.round() as i32;
141 let row = py.round() as i32;
142
143 let icon_w = icon.chars().count() as i32;
144 if col < play_area.x as i32
145 || col + icon_w > (play_area.x + play_area.width) as i32
146 || row < play_area.y as i32
147 || row >= (play_area.y + play_area.height) as i32
148 {
149 continue;
150 }
151 let bx = biscuit.x as i32;
152 let br = bx + biscuit.width as i32;
153 let by = biscuit.y as i32;
154 let bb = by + biscuit.height as i32;
155 if row >= by && row < bb && col < br && col + icon_w > bx {
156 continue;
157 }
158 let color = hand_color(*type_idx, poke > 0.0);
159 buf.set_string(col as u16, row as u16, *icon, Style::default().fg(color));
160 }
161}
162
163fn hand_color(type_idx: usize, poking: bool) -> Color {
164 let palette = [
165 Color::Rgb(220, 170, 130), Color::Rgb(230, 180, 160), Color::Rgb(200, 230, 220), Color::Rgb(255, 130, 170), Color::Rgb(180, 200, 230), Color::Rgb(200, 160, 220), Color::Rgb(255, 200, 120), Color::Rgb(140, 180, 255), Color::Rgb(255, 180, 255), Color::Rgb(255, 230, 150), ];
176 let c = palette[type_idx.min(palette.len() - 1)];
177 if poking { brighten(c) } else { c }
178}
179
180fn brighten(c: Color) -> Color {
181 match c {
182 Color::Rgb(r, g, b) => Color::Rgb(
183 (r as u16 + 40).min(255) as u8,
184 (g as u16 + 40).min(255) as u8,
185 (b as u16 + 40).min(255) as u8,
186 ),
187 other => other,
188 }
189}