summaryrefslogtreecommitdiffstats
path: root/gui/src/roulette.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--gui/src/roulette.rs161
1 files changed, 97 insertions, 64 deletions
diff --git a/gui/src/roulette.rs b/gui/src/roulette.rs
index 22c46d6..f674251 100644
--- a/gui/src/roulette.rs
+++ b/gui/src/roulette.rs
@@ -7,10 +7,10 @@
7use std::f32::consts::{FRAC_PI_2, TAU}; 7use std::f32::consts::{FRAC_PI_2, TAU};
8 8
9use eframe::egui::{ 9use eframe::egui::{
10 Align2, Color32, CornerRadius, Painter, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2, 10 Align2, Color32, CornerRadius, Painter, PointerButton, Pos2, Rect, Sense, Shape, Stroke,
11 pos2, vec2, 11 StrokeKind, Ui, Vec2, pos2, vec2,
12}; 12};
13use zagan_bet::client::games::roulette::Cursor; 13use zagan_bet::client::games::roulette::target_at as lattice_target;
14use zagan_bet::client::view::{RoulettePhase, RouletteView}; 14use zagan_bet::client::view::{RoulettePhase, RouletteView};
15use zagan_bet::roulette::{BetTarget, Outside, WHEEL, color as wheel_color, is_red}; 15use zagan_bet::roulette::{BetTarget, Outside, WHEEL, color as wheel_color, is_red};
16 16
@@ -42,18 +42,38 @@ pub fn size_for(available: Vec2) -> Vec2 {
42 vec2((COLUMNS as f32 + 1.0) * cell, HEIGHT_UNITS * cell) + Vec2::splat(BOARD_PAD * 2.0) 42 vec2((COLUMNS as f32 + 1.0) * cell, HEIGHT_UNITS * cell) + Vec2::splat(BOARD_PAD * 2.0)
43} 43}
44 44
45/// The table painted at `size`; returns the target clicked this frame. 45/// What the pointer did to the felt this frame.
46pub fn felt(ui: &mut Ui, size: Vec2, view: &RouletteView) -> Option<BetTarget> { 46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum FeltAction {
48 /// Left click: add the selected chip.
49 Place(BetTarget),
50 /// Right or middle click: take one chip back.
51 Reduce(BetTarget),
52}
53
54/// The table painted at `size`; returns the bet the pointer changed.
55pub fn felt(ui: &mut Ui, size: Vec2, view: &RouletteView) -> Option<FeltAction> {
47 let (rect, response) = ui.allocate_exact_size(size, Sense::click()); 56 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
48 let felt = Felt::new(rect); 57 let felt = Felt::new(rect);
49 let hovered = response.hover_pos().and_then(|pos| felt.hit(pos)); 58 let hovered = if view.phase == RoulettePhase::Spinning {
59 None
60 } else {
61 response.hover_pos().and_then(|pos| felt.target_at(pos))
62 };
50 let painter = ui.painter(); 63 let painter = ui.painter();
51 64
52 painter.rect_filled(rect, theme::RADIUS_LG, theme::FELT_DARK); 65 painter.rect_filled(rect, theme::RADIUS_LG, theme::FELT_DARK);
53 66
67 // Every cell the hovered bet covers tints, so a split or corner reads
68 // before the chip lands.
69 let covered = hovered.as_ref().map(BetTarget::numbers).unwrap_or_default();
54 for number in 0..=36u8 { 70 for number in 0..=36u8 {
55 let highlighted = hovered == Some(BetTarget::Straight { number }); 71 felt.paint_number(
56 felt.paint_number(painter, number, highlighted); 72 painter,
73 number,
74 covered.contains(&number),
75 hovered == Some(BetTarget::Straight { number }),
76 );
57 } 77 }
58 for index in 0..3 { 78 for index in 0..3 {
59 felt.paint_dozen( 79 felt.paint_dozen(
@@ -89,24 +109,38 @@ pub fn felt(ui: &mut Ui, size: Vec2, view: &RouletteView) -> Option<BetTarget> {
89 } 109 }
90 } 110 }
91 111
92 felt.paint_cursor(painter, &view.cursor);
93
94 for chip in &view.bets.chips { 112 for chip in &view.bets.chips {
95 if let Some(center) = felt.anchor(&chip.target) { 113 if let Some(center) = felt.anchor(&chip.target) {
96 paint_chip(painter, center, felt.cell * 0.26, chip.amount); 114 paint_chip(painter, center, felt.cell_size() * 0.26, chip.amount);
115 }
116 }
117
118 if let Some(target) = &hovered {
119 if let Some(center) = felt.anchor(target) {
120 paint_chip_preview(painter, center, felt.cell_size() * 0.26, view.chip);
97 } 121 }
98 } 122 }
99 123
100 if response.clicked() { hovered } else { None } 124 if response.clicked_by(PointerButton::Secondary) || response.clicked_by(PointerButton::Middle) {
125 hovered.map(FeltAction::Reduce)
126 } else if response.clicked() {
127 hovered.map(FeltAction::Place)
128 } else {
129 None
130 }
101} 131}
102 132
103struct Felt { 133/// Geometry of one table: the felt is 13 cells wide (zero plus twelve
134/// columns) and 3 + 0.72 + 0.55 + 0.62 cells tall.
135///
136/// Public so the pointer snapping can be tested without a window.
137pub struct Felt {
104 cell: f32, 138 cell: f32,
105 origin: Pos2, 139 origin: Pos2,
106} 140}
107 141
108impl Felt { 142impl Felt {
109 fn new(board: Rect) -> Self { 143 pub fn new(board: Rect) -> Self {
110 let inner = board.shrink(BOARD_PAD); 144 let inner = board.shrink(BOARD_PAD);
111 Self { 145 Self {
112 cell: inner.width() / (COLUMNS as f32 + 1.0), 146 cell: inner.width() / (COLUMNS as f32 + 1.0),
@@ -114,7 +148,11 @@ impl Felt {
114 } 148 }
115 } 149 }
116 150
117 fn number_rect(&self, number: u8) -> Rect { 151 pub fn cell_size(&self) -> f32 {
152 self.cell
153 }
154
155 pub fn number_rect(&self, number: u8) -> Rect {
118 debug_assert!(number <= 36); 156 debug_assert!(number <= 36);
119 if number == 0 { 157 if number == 0 {
120 return Rect::from_min_size(self.origin, vec2(self.cell, self.cell * ROWS as f32)); 158 return Rect::from_min_size(self.origin, vec2(self.cell, self.cell * ROWS as f32));
@@ -138,8 +176,8 @@ impl Felt {
138 fn column_rect(&self, index: usize) -> Rect { 176 fn column_rect(&self, index: usize) -> Rect {
139 let y = self.origin.y + self.cell * (ROWS as f32 + DOZEN_ROW); 177 let y = self.origin.y + self.cell * (ROWS as f32 + DOZEN_ROW);
140 Rect::from_min_size( 178 Rect::from_min_size(
141 pos2(self.origin.x + self.cell * (1.0 + index as f32), y), 179 pos2(self.origin.x + self.cell * (1.0 + 4.0 * index as f32), y),
142 vec2(self.cell, self.cell * COLUMN_ROW), 180 vec2(self.cell * 4.0, self.cell * COLUMN_ROW),
143 ) 181 )
144 } 182 }
145 183
@@ -152,7 +190,7 @@ impl Felt {
152 } 190 }
153 191
154 /// Where a chip sits for this target. 192 /// Where a chip sits for this target.
155 fn anchor(&self, target: &BetTarget) -> Option<Pos2> { 193 pub fn anchor(&self, target: &BetTarget) -> Option<Pos2> {
156 Some(match target { 194 Some(match target {
157 BetTarget::Straight { number } => self.number_rect(*number).center(), 195 BetTarget::Straight { number } => self.number_rect(*number).center(),
158 BetTarget::Split { a, b } => { 196 BetTarget::Split { a, b } => {
@@ -178,20 +216,28 @@ impl Felt {
178 }) 216 })
179 } 217 }
180 218
181 /// The fine-grid point the cursor occupies over the numbers. 219 fn grid_origin(&self) -> Pos2 {
182 fn lattice(&self, x: usize, y: usize) -> Pos2 { 220 self.origin + vec2(self.cell, 0.0)
183 pos2(
184 self.origin.x + self.cell * (1.0 + x as f32 * 0.5),
185 self.origin.y + self.cell * (y as f32 * 0.5),
186 )
187 } 221 }
188 222
189 fn hit(&self, pos: Pos2) -> Option<BetTarget> { 223 /// The bet under the pointer, snapped to the fine lattice: a cell centre
190 for number in 0..=36u8 { 224 /// is a straight, a line is a split, a crossing is a corner.
191 if self.number_rect(number).contains(pos) { 225 pub fn target_at(&self, pos: Pos2) -> Option<BetTarget> {
192 return Some(BetTarget::Straight { number }); 226 if self.number_rect(0).contains(pos) {
193 } 227 return Some(BetTarget::Straight { number: 0 });
228 }
229
230 let grid = self.grid_origin();
231 let fx = (pos.x - grid.x) / self.cell;
232 let fy = (pos.y - grid.y) / self.cell;
233 if (0.0..COLUMNS as f32).contains(&fx) && (0.0..ROWS as f32).contains(&fy) {
234 // Thresholds sit a quarter cell from a centre, so the inner half
235 // of a cell is a straight and its edges are splits or corners.
236 let x = (fx * 2.0).round().clamp(1.0, 23.0) as usize;
237 let y = (fy * 2.0).round().clamp(1.0, 5.0) as usize;
238 return lattice_target(x, y);
194 } 239 }
240
195 for index in 0..3usize { 241 for index in 0..3usize {
196 if self.dozen_rect(index).contains(pos) { 242 if self.dozen_rect(index).contains(pos) {
197 return Some(BetTarget::Dozen { index: index as u8 }); 243 return Some(BetTarget::Dozen { index: index as u8 });
@@ -208,7 +254,7 @@ impl Felt {
208 None 254 None
209 } 255 }
210 256
211 fn paint_number(&self, painter: &Painter, number: u8, hovered: bool) { 257 fn paint_number(&self, painter: &Painter, number: u8, covered: bool, hovered: bool) {
212 let rect = self.number_rect(number); 258 let rect = self.number_rect(number);
213 let fill = if number == 0 { 259 let fill = if number == 0 {
214 theme::ROULETTE_ZERO 260 theme::ROULETTE_ZERO
@@ -224,7 +270,9 @@ impl Felt {
224 Stroke::new(1.0, theme::FELT_LINE), 270 Stroke::new(1.0, theme::FELT_LINE),
225 StrokeKind::Inside, 271 StrokeKind::Inside,
226 ); 272 );
227 if hovered { 273 if covered {
274 painter.rect_filled(rect, CornerRadius::ZERO, theme::ACCENT.gamma_multiply(0.16));
275 } else if hovered {
228 painter.rect_filled( 276 painter.rect_filled(
229 rect, 277 rect,
230 CornerRadius::ZERO, 278 CornerRadius::ZERO,
@@ -303,28 +351,6 @@ impl Felt {
303 } 351 }
304 } 352 }
305 353
306 fn paint_cursor(&self, painter: &Painter, cursor: &Cursor) {
307 let stroke = Stroke::new(2.0, theme::ACCENT);
308 match cursor {
309 Cursor::Zero => outline(painter, self.number_rect(0), stroke),
310 Cursor::Lattice { x, y } => {
311 for number in cursor.numbers() {
312 painter.rect_filled(
313 self.number_rect(number),
314 CornerRadius::ZERO,
315 theme::ACCENT.gamma_multiply(0.14),
316 );
317 }
318 let point = self.lattice(*x, *y);
319 painter.circle_filled(point, self.cell * 0.05, theme::ACCENT);
320 painter.circle_stroke(point, self.cell * 0.14, stroke);
321 }
322 Cursor::Dozen(index) => outline(painter, self.dozen_rect(*index), stroke),
323 Cursor::Column(index) => outline(painter, self.column_rect(*index), stroke),
324 Cursor::Outside(index) => outline(painter, self.outside_rect(*index), stroke),
325 }
326 }
327
328 fn number_font(&self) -> f32 { 354 fn number_font(&self) -> f32 {
329 (self.cell * 0.40).clamp(12.0, 22.0) 355 (self.cell * 0.40).clamp(12.0, 22.0)
330 } 356 }
@@ -334,15 +360,6 @@ impl Felt {
334 } 360 }
335} 361}
336 362
337fn outline(painter: &Painter, rect: Rect, stroke: Stroke) {
338 painter.rect_stroke(
339 rect.shrink(1.0),
340 theme::RADIUS_SM,
341 stroke,
342 StrokeKind::Inside,
343 );
344}
345
346/// A single chip, also used by the sidebar's chip tray. 363/// A single chip, also used by the sidebar's chip tray.
347pub fn paint_chip(painter: &Painter, center: Pos2, radius: f32, amount: u64) { 364pub fn paint_chip(painter: &Painter, center: Pos2, radius: f32, amount: u64) {
348 let fill = chip_color(amount); 365 let fill = chip_color(amount);
@@ -366,6 +383,23 @@ pub fn paint_chip(painter: &Painter, center: Pos2, radius: f32, amount: u64) {
366 ); 383 );
367} 384}
368 385
386/// The chip under the pointer, showing what a click would add.
387pub fn paint_chip_preview(painter: &Painter, center: Pos2, radius: f32, amount: u64) {
388 painter.circle_filled(center, radius, chip_color(amount).gamma_multiply(0.45));
389 painter.circle_stroke(
390 center,
391 radius,
392 Stroke::new(2.0, Color32::WHITE.gamma_multiply(0.85)),
393 );
394 painter.text(
395 center,
396 Align2::CENTER_CENTER,
397 amount,
398 theme::mono(radius * 0.85),
399 Color32::WHITE,
400 );
401}
402
369/// Colour per denomination, as a chip tray would be stocked. 403/// Colour per denomination, as a chip tray would be stocked.
370pub fn chip_color(amount: u64) -> Color32 { 404pub fn chip_color(amount: u64) -> Color32 {
371 match amount { 405 match amount {
@@ -444,9 +478,8 @@ pub fn wheel(ui: &mut Ui, diameter: f32, view: &RouletteView, rotation: f32) {
444 painter.circle_stroke(center, inner, Stroke::new(1.0, theme::LINE)); 478 painter.circle_stroke(center, inner, Stroke::new(1.0, theme::LINE));
445 479
446 let displayed = match view.phase { 480 let displayed = match view.phase {
447 RoulettePhase::Result => view.number,
448 RoulettePhase::Spinning => Some(number_under(rotation)), 481 RoulettePhase::Spinning => Some(number_under(rotation)),
449 RoulettePhase::Placing => None, 482 _ => view.number,
450 }; 483 };
451 match displayed { 484 match displayed {
452 Some(number) => { 485 Some(number) => {