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(
19 col: u16,
20 row: u16,
21 biscuit: Rect,
22 focal: (u16, u16),
23 state: &GameState,
24) -> bool {
25 if biscuit.width == 0 || biscuit.height == 0 {
26 return false;
27 }
28 let cx = focal.0 as f32;
34 let cy = focal.1 as f32;
35 let base_rx = (biscuit.width as f32 / 2.0 + 3.0).max(6.0);
36 let base_ry = (biscuit.height as f32 / 2.0 + 2.0).max(3.0);
37
38 let mut glyphs: Vec<(usize, &str)> = Vec::new();
39 for (idx, f) in FINGERERS.iter().enumerate() {
40 let n = (state.fingerer_count(f.id) as usize).min(PER_TYPE_CAP);
41 for _ in 0..n {
42 glyphs.push((idx, f.icon));
43 }
44 }
45 if glyphs.is_empty() {
46 return false;
47 }
48
49 let total = glyphs.len();
50 let bx = biscuit.x as i32;
51 let br = bx + biscuit.width as i32;
52 let by = biscuit.y as i32;
53 let bb = by + biscuit.height as i32;
54
55 for (i, (type_idx, icon)) in glyphs.iter().enumerate() {
56 let ring = i / PER_RING;
57 let slot = i % PER_RING;
58 let slot_count = ((total - ring * PER_RING).min(PER_RING)) as f32;
59 let angle = (slot as f32 / slot_count) * std::f32::consts::TAU
60 + (ring as f32 * 0.15)
61 + (*type_idx as f32 * 0.07);
62 let speed = FINGERERS
67 .get(*type_idx)
68 .map(|f| f.poke_speed.max(0.1))
69 .unwrap_or(1.0);
70 let tier_divisor = (5.0 / speed).max(1.0) as u64;
71 let tier_phase = state.session_ticks / tier_divisor;
72 let poke = if ((i * 7) as u64 + tier_phase).is_multiple_of(23) {
73 1.2
74 } else {
75 0.0
76 };
77 let rx = base_rx + ring as f32 * 4.0 - poke;
78 let ry = base_ry + ring as f32 * 2.0 - poke * 0.5;
79 let px = cx + rx * angle.cos();
80 let py = cy + ry * angle.sin();
81 let g_col = px.round() as i32;
82 let g_row = py.round() as i32;
83
84 let icon_w = icon.chars().count() as i32;
85 if g_row >= by && g_row < bb && g_col < br && g_col + icon_w > bx {
88 continue;
89 }
90 if (row as i32) == g_row && (col as i32) >= g_col && (col as i32) < g_col + icon_w {
92 return true;
93 }
94 }
95 false
96}
97
98pub fn draw(
99 frame: &mut Frame,
100 play_area: Rect,
101 biscuit: Rect,
102 focal: (u16, u16),
103 state: &GameState,
104) {
105 if play_area.width == 0 || play_area.height == 0 {
106 return;
107 }
108 let buf = frame.buffer_mut();
109 let cx = focal.0 as f32;
111 let cy = focal.1 as f32;
112
113 let base_rx = (biscuit.width as f32 / 2.0 + 3.0).max(6.0);
114 let base_ry = (biscuit.height as f32 / 2.0 + 2.0).max(3.0);
115
116 let mut glyphs: Vec<(usize, &str)> = Vec::new();
117 for (idx, f) in FINGERERS.iter().enumerate() {
118 let n = (state.fingerer_count(f.id) as usize).min(PER_TYPE_CAP);
119 for _ in 0..n {
120 glyphs.push((idx, f.icon));
121 }
122 }
123 if glyphs.is_empty() {
124 return;
125 }
126
127 let total = glyphs.len();
128
129 for (i, (type_idx, icon)) in glyphs.iter().enumerate() {
130 let ring = i / PER_RING;
131 let slot = i % PER_RING;
132 let slot_count = ((total - ring * PER_RING).min(PER_RING)) as f32;
133 let angle = (slot as f32 / slot_count) * std::f32::consts::TAU
134 + (ring as f32 * 0.15)
135 + (*type_idx as f32 * 0.07);
136 let speed = FINGERERS
144 .get(*type_idx)
145 .map(|f| f.poke_speed.max(0.1))
146 .unwrap_or(1.0);
147 let tier_divisor = (5.0 / speed).max(1.0) as u64;
148 let tier_phase = state.session_ticks / tier_divisor;
149 let poke = if ((i * 7) as u64 + tier_phase).is_multiple_of(23) {
150 1.2
151 } else {
152 0.0
153 };
154 let rx = base_rx + ring as f32 * 4.0 - poke;
155 let ry = base_ry + ring as f32 * 2.0 - poke * 0.5;
156 let px = cx + rx * angle.cos();
157 let py = cy + ry * angle.sin();
158 let col = px.round() as i32;
159 let row = py.round() as i32;
160
161 let icon_w = icon.chars().count() as i32;
162 if col < play_area.x as i32
163 || col + icon_w > (play_area.x + play_area.width) as i32
164 || row < play_area.y as i32
165 || row >= (play_area.y + play_area.height) as i32
166 {
167 continue;
168 }
169 let bx = biscuit.x as i32;
170 let br = bx + biscuit.width as i32;
171 let by = biscuit.y as i32;
172 let bb = by + biscuit.height as i32;
173 if row >= by && row < bb && col < br && col + icon_w > bx {
174 continue;
175 }
176 let color = hand_color(*type_idx, poke > 0.0);
177 buf.set_string(col as u16, row as u16, *icon, Style::default().fg(color));
178 }
179}
180
181fn hand_color(type_idx: usize, poking: bool) -> Color {
182 let palette = [
183 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), ];
194 let c = palette[type_idx.min(palette.len() - 1)];
195 if poking { brighten(c) } else { c }
196}
197
198fn brighten(c: Color) -> Color {
199 match c {
200 Color::Rgb(r, g, b) => Color::Rgb(
201 (r as u16 + 40).min(255) as u8,
202 (g as u16 + 40).min(255) as u8,
203 (b as u16 + 40).min(255) as u8,
204 ),
205 other => other,
206 }
207}