From e920b20fafe375eec15b24eb8cde4b1455dd01af Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 20:11:41 +0100 Subject: v4.1.1: pointer-first betting and visible payouts in the GUI The felt snaps the hover to the shared fine lattice: inner half of a cell is a straight, an edge a split, a crossing a corner. A translucent chip previews the target and the covered cells tint. Left click adds the selected chip, right or middle click takes one back through the new Bets::reduce; C or CLEAR empties the slip, and SPIN stays disabled with no chips. The result now pays out: the wheel leaves the winning pocket under the pointer, the winning cell and pocket get a gold outline, and the sidebar reports the amount returned and the net for the round (NO WIN on a loss). Placing a chip after a result starts the next round without losing the wheel position. The column row now spans the full width like a real table instead of hugging the left edge. Adds tests for lattice snapping, target naming and taking chips back. --- gui/src/roulette.rs | 161 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 64 deletions(-) (limited to 'gui/src/roulette.rs') 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 @@ use std::f32::consts::{FRAC_PI_2, TAU}; use eframe::egui::{ - Align2, Color32, CornerRadius, Painter, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, Vec2, - pos2, vec2, + Align2, Color32, CornerRadius, Painter, PointerButton, Pos2, Rect, Sense, Shape, Stroke, + StrokeKind, Ui, Vec2, pos2, vec2, }; -use zagan_bet::client::games::roulette::Cursor; +use zagan_bet::client::games::roulette::target_at as lattice_target; use zagan_bet::client::view::{RoulettePhase, RouletteView}; use zagan_bet::roulette::{BetTarget, Outside, WHEEL, color as wheel_color, is_red}; @@ -42,18 +42,38 @@ pub fn size_for(available: Vec2) -> Vec2 { vec2((COLUMNS as f32 + 1.0) * cell, HEIGHT_UNITS * cell) + Vec2::splat(BOARD_PAD * 2.0) } -/// The table painted at `size`; returns the target clicked this frame. -pub fn felt(ui: &mut Ui, size: Vec2, view: &RouletteView) -> Option { +/// What the pointer did to the felt this frame. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FeltAction { + /// Left click: add the selected chip. + Place(BetTarget), + /// Right or middle click: take one chip back. + Reduce(BetTarget), +} + +/// The table painted at `size`; returns the bet the pointer changed. +pub fn felt(ui: &mut Ui, size: Vec2, view: &RouletteView) -> Option { let (rect, response) = ui.allocate_exact_size(size, Sense::click()); let felt = Felt::new(rect); - let hovered = response.hover_pos().and_then(|pos| felt.hit(pos)); + let hovered = if view.phase == RoulettePhase::Spinning { + None + } else { + response.hover_pos().and_then(|pos| felt.target_at(pos)) + }; let painter = ui.painter(); painter.rect_filled(rect, theme::RADIUS_LG, theme::FELT_DARK); + // Every cell the hovered bet covers tints, so a split or corner reads + // before the chip lands. + let covered = hovered.as_ref().map(BetTarget::numbers).unwrap_or_default(); for number in 0..=36u8 { - let highlighted = hovered == Some(BetTarget::Straight { number }); - felt.paint_number(painter, number, highlighted); + felt.paint_number( + painter, + number, + covered.contains(&number), + hovered == Some(BetTarget::Straight { number }), + ); } for index in 0..3 { felt.paint_dozen( @@ -89,24 +109,38 @@ pub fn felt(ui: &mut Ui, size: Vec2, view: &RouletteView) -> Option { } } - felt.paint_cursor(painter, &view.cursor); - for chip in &view.bets.chips { if let Some(center) = felt.anchor(&chip.target) { - paint_chip(painter, center, felt.cell * 0.26, chip.amount); + paint_chip(painter, center, felt.cell_size() * 0.26, chip.amount); + } + } + + if let Some(target) = &hovered { + if let Some(center) = felt.anchor(target) { + paint_chip_preview(painter, center, felt.cell_size() * 0.26, view.chip); } } - if response.clicked() { hovered } else { None } + if response.clicked_by(PointerButton::Secondary) || response.clicked_by(PointerButton::Middle) { + hovered.map(FeltAction::Reduce) + } else if response.clicked() { + hovered.map(FeltAction::Place) + } else { + None + } } -struct Felt { +/// Geometry of one table: the felt is 13 cells wide (zero plus twelve +/// columns) and 3 + 0.72 + 0.55 + 0.62 cells tall. +/// +/// Public so the pointer snapping can be tested without a window. +pub struct Felt { cell: f32, origin: Pos2, } impl Felt { - fn new(board: Rect) -> Self { + pub fn new(board: Rect) -> Self { let inner = board.shrink(BOARD_PAD); Self { cell: inner.width() / (COLUMNS as f32 + 1.0), @@ -114,7 +148,11 @@ impl Felt { } } - fn number_rect(&self, number: u8) -> Rect { + pub fn cell_size(&self) -> f32 { + self.cell + } + + pub fn number_rect(&self, number: u8) -> Rect { debug_assert!(number <= 36); if number == 0 { return Rect::from_min_size(self.origin, vec2(self.cell, self.cell * ROWS as f32)); @@ -138,8 +176,8 @@ impl Felt { fn column_rect(&self, index: usize) -> Rect { let y = self.origin.y + self.cell * (ROWS as f32 + DOZEN_ROW); Rect::from_min_size( - pos2(self.origin.x + self.cell * (1.0 + index as f32), y), - vec2(self.cell, self.cell * COLUMN_ROW), + pos2(self.origin.x + self.cell * (1.0 + 4.0 * index as f32), y), + vec2(self.cell * 4.0, self.cell * COLUMN_ROW), ) } @@ -152,7 +190,7 @@ impl Felt { } /// Where a chip sits for this target. - fn anchor(&self, target: &BetTarget) -> Option { + pub fn anchor(&self, target: &BetTarget) -> Option { Some(match target { BetTarget::Straight { number } => self.number_rect(*number).center(), BetTarget::Split { a, b } => { @@ -178,20 +216,28 @@ impl Felt { }) } - /// The fine-grid point the cursor occupies over the numbers. - fn lattice(&self, x: usize, y: usize) -> Pos2 { - pos2( - self.origin.x + self.cell * (1.0 + x as f32 * 0.5), - self.origin.y + self.cell * (y as f32 * 0.5), - ) + fn grid_origin(&self) -> Pos2 { + self.origin + vec2(self.cell, 0.0) } - fn hit(&self, pos: Pos2) -> Option { - for number in 0..=36u8 { - if self.number_rect(number).contains(pos) { - return Some(BetTarget::Straight { number }); - } + /// The bet under the pointer, snapped to the fine lattice: a cell centre + /// is a straight, a line is a split, a crossing is a corner. + pub fn target_at(&self, pos: Pos2) -> Option { + if self.number_rect(0).contains(pos) { + return Some(BetTarget::Straight { number: 0 }); + } + + let grid = self.grid_origin(); + let fx = (pos.x - grid.x) / self.cell; + let fy = (pos.y - grid.y) / self.cell; + if (0.0..COLUMNS as f32).contains(&fx) && (0.0..ROWS as f32).contains(&fy) { + // Thresholds sit a quarter cell from a centre, so the inner half + // of a cell is a straight and its edges are splits or corners. + let x = (fx * 2.0).round().clamp(1.0, 23.0) as usize; + let y = (fy * 2.0).round().clamp(1.0, 5.0) as usize; + return lattice_target(x, y); } + for index in 0..3usize { if self.dozen_rect(index).contains(pos) { return Some(BetTarget::Dozen { index: index as u8 }); @@ -208,7 +254,7 @@ impl Felt { None } - fn paint_number(&self, painter: &Painter, number: u8, hovered: bool) { + fn paint_number(&self, painter: &Painter, number: u8, covered: bool, hovered: bool) { let rect = self.number_rect(number); let fill = if number == 0 { theme::ROULETTE_ZERO @@ -224,7 +270,9 @@ impl Felt { Stroke::new(1.0, theme::FELT_LINE), StrokeKind::Inside, ); - if hovered { + if covered { + painter.rect_filled(rect, CornerRadius::ZERO, theme::ACCENT.gamma_multiply(0.16)); + } else if hovered { painter.rect_filled( rect, CornerRadius::ZERO, @@ -303,28 +351,6 @@ impl Felt { } } - fn paint_cursor(&self, painter: &Painter, cursor: &Cursor) { - let stroke = Stroke::new(2.0, theme::ACCENT); - match cursor { - Cursor::Zero => outline(painter, self.number_rect(0), stroke), - Cursor::Lattice { x, y } => { - for number in cursor.numbers() { - painter.rect_filled( - self.number_rect(number), - CornerRadius::ZERO, - theme::ACCENT.gamma_multiply(0.14), - ); - } - let point = self.lattice(*x, *y); - painter.circle_filled(point, self.cell * 0.05, theme::ACCENT); - painter.circle_stroke(point, self.cell * 0.14, stroke); - } - Cursor::Dozen(index) => outline(painter, self.dozen_rect(*index), stroke), - Cursor::Column(index) => outline(painter, self.column_rect(*index), stroke), - Cursor::Outside(index) => outline(painter, self.outside_rect(*index), stroke), - } - } - fn number_font(&self) -> f32 { (self.cell * 0.40).clamp(12.0, 22.0) } @@ -334,15 +360,6 @@ impl Felt { } } -fn outline(painter: &Painter, rect: Rect, stroke: Stroke) { - painter.rect_stroke( - rect.shrink(1.0), - theme::RADIUS_SM, - stroke, - StrokeKind::Inside, - ); -} - /// A single chip, also used by the sidebar's chip tray. pub fn paint_chip(painter: &Painter, center: Pos2, radius: f32, amount: u64) { let fill = chip_color(amount); @@ -366,6 +383,23 @@ pub fn paint_chip(painter: &Painter, center: Pos2, radius: f32, amount: u64) { ); } +/// The chip under the pointer, showing what a click would add. +pub fn paint_chip_preview(painter: &Painter, center: Pos2, radius: f32, amount: u64) { + painter.circle_filled(center, radius, chip_color(amount).gamma_multiply(0.45)); + painter.circle_stroke( + center, + radius, + Stroke::new(2.0, Color32::WHITE.gamma_multiply(0.85)), + ); + painter.text( + center, + Align2::CENTER_CENTER, + amount, + theme::mono(radius * 0.85), + Color32::WHITE, + ); +} + /// Colour per denomination, as a chip tray would be stocked. pub fn chip_color(amount: u64) -> Color32 { match amount { @@ -444,9 +478,8 @@ pub fn wheel(ui: &mut Ui, diameter: f32, view: &RouletteView, rotation: f32) { painter.circle_stroke(center, inner, Stroke::new(1.0, theme::LINE)); let displayed = match view.phase { - RoulettePhase::Result => view.number, RoulettePhase::Spinning => Some(number_under(rotation)), - RoulettePhase::Placing => None, + _ => view.number, }; match displayed { Some(number) => { -- cgit v1.2.3