diff --git a/client_web/assets/style.css b/client_web/assets/style.css index ab34c7e..2edad91 100644 --- a/client_web/assets/style.css +++ b/client_web/assets/style.css @@ -262,7 +262,7 @@ input[type="text"] { /* ── Fields ─────────────────────────────────────────────────────────── */ .field { width: 60px; - height: 110px; + height: 180px; background: #d4a843; border-radius: 4px; display: flex; @@ -295,25 +295,34 @@ input[type="text"] { .top-row .field-num { bottom: auto; top: 2px; } /* ── Checkers ───────────────────────────────────────────────────────── */ -.checkers { - width: 46px; - height: 46px; +.checker-stack { + display: flex; + flex-direction: column; + align-items: center; +} + +.checker { + width: 40px; + height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - font-size: 1rem; + font-size: 0.8rem; font-weight: bold; border: 2px solid rgba(0,0,0,0.3); - box-shadow: inset 0 2px 4px rgba(255,255,255,0.3), 0 2px 4px rgba(0,0,0,0.3); + box-shadow: inset 0 2px 4px rgba(255,255,255,0.3), 0 1px 3px rgba(0,0,0,0.3); + flex-shrink: 0; } -.checkers.white { +.checker + .checker { margin-top: 2px; } + +.checker.white { background: radial-gradient(circle at 35% 35%, #ffffff, #cccccc); color: #333; } -.checkers.black { +.checker.black { background: radial-gradient(circle at 35% 35%, #555555, #111111); color: #eee; } diff --git a/client_web/src/components/board.rs b/client_web/src/components/board.rs index a0ae393..0ec3040 100644 --- a/client_web/src/components/board.rs +++ b/client_web/src/components/board.rs @@ -3,15 +3,15 @@ use leptos::prelude::*; use crate::trictrac::types::{SerTurnStage, ViewState}; /// Field numbers in visual display order (left-to-right for each quarter), white's perspective. -const TOP_LEFT_W: [u8; 6] = [13, 14, 15, 16, 17, 18]; +const TOP_LEFT_W: [u8; 6] = [13, 14, 15, 16, 17, 18]; const TOP_RIGHT_W: [u8; 6] = [19, 20, 21, 22, 23, 24]; -const BOT_LEFT_W: [u8; 6] = [12, 11, 10, 9, 8, 7]; -const BOT_RIGHT_W: [u8; 6] = [ 6, 5, 4, 3, 2, 1]; +const BOT_LEFT_W: [u8; 6] = [12, 11, 10, 9, 8, 7]; +const BOT_RIGHT_W: [u8; 6] = [6, 5, 4, 3, 2, 1]; /// 180° rotation of white's layout: black's pieces (field 24) appear at the bottom. -const TOP_LEFT_B: [u8; 6] = [ 1, 2, 3, 4, 5, 6]; -const TOP_RIGHT_B: [u8; 6] = [ 7, 8, 9, 10, 11, 12]; -const BOT_LEFT_B: [u8; 6] = [24, 23, 22, 21, 20, 19]; +const TOP_LEFT_B: [u8; 6] = [1, 2, 3, 4, 5, 6]; +const TOP_RIGHT_B: [u8; 6] = [7, 8, 9, 10, 11, 12]; +const BOT_LEFT_B: [u8; 6] = [24, 23, 22, 21, 20, 19]; const BOT_RIGHT_B: [u8; 6] = [18, 17, 16, 15, 14, 13]; /// Returns the displayed board value for `field_num` after applying `staged_moves`. @@ -25,8 +25,12 @@ fn displayed_value( let mut val = base_board[(field_num - 1) as usize]; let delta: i8 = if is_white { 1 } else { -1 }; for &(from, to) in staged_moves { - if from == field_num { val -= delta; } - if to == field_num { val += delta; } + if from == field_num { + val -= delta; + } + if to == field_num { + val += delta; + } } val } @@ -42,66 +46,83 @@ pub fn Board( ) -> impl IntoView { let board = view_state.board; let is_move_stage = view_state.active_mp_player == Some(player_id) - && matches!(view_state.turn_stage, SerTurnStage::Move | SerTurnStage::HoldOrGoChoice); + && matches!( + view_state.turn_stage, + SerTurnStage::Move | SerTurnStage::HoldOrGoChoice + ); let is_white = player_id == 0; - let fields_from = |nums: &[u8]| -> Vec { - nums.iter().map(|&field_num| { - view! { -
0 } else { val < 0 }; - let can_stage = is_move_stage && moves.len() < 2; - let sel = selected_origin.get(); + let fields_from = |nums: &[u8], is_top_row: bool| -> Vec { + nums.iter() + .map(|&field_num| { + view! { +
0 } else { val < 0 }; + let can_stage = is_move_stage && moves.len() < 2; + let sel = selected_origin.get(); - let mut cls = "field".to_string(); - if can_stage && (sel.is_some() || is_mine) { - cls.push_str(" clickable"); - } - if sel == Some(field_num) { cls.push_str(" selected"); } - if can_stage && sel.is_some() && sel != Some(field_num) { - cls.push_str(" dest"); - } - cls - } - on:click=move |_| { - if !is_move_stage { return; } - if staged_moves.get_untracked().len() >= 2 { return; } - - let moves = staged_moves.get_untracked(); - let val = displayed_value(board, &moves, is_white, field_num); - let is_mine = if is_white { val > 0 } else { val < 0 }; - - match selected_origin.get_untracked() { - Some(origin) if origin == field_num => { - selected_origin.set(None); + let mut cls = "field".to_string(); + if can_stage && (sel.is_some() || is_mine) { + cls.push_str(" clickable"); } - Some(origin) => { - staged_moves.update(|v| v.push((origin, field_num))); - selected_origin.set(None); + if sel == Some(field_num) { cls.push_str(" selected"); } + if can_stage && sel.is_some() && sel != Some(field_num) { + cls.push_str(" dest"); } - None if is_mine => selected_origin.set(Some(field_num)), - None => {} + cls } - } - > - {field_num} - {move || { - let moves = staged_moves.get(); - let val = displayed_value(board, &moves, is_white, field_num); - let count = val.unsigned_abs(); - (count > 0).then(|| { - let color = if val > 0 { "white" } else { "black" }; - view! { {count} } - }) - }} -
- } - .into_any() - }) - .collect() + on:click=move |_| { + if !is_move_stage { return; } + if staged_moves.get_untracked().len() >= 2 { return; } + + let moves = staged_moves.get_untracked(); + let val = displayed_value(board, &moves, is_white, field_num); + let is_mine = if is_white { val > 0 } else { val < 0 }; + + match selected_origin.get_untracked() { + Some(origin) if origin == field_num => { + selected_origin.set(None); + } + Some(origin) => { + staged_moves.update(|v| v.push((origin, field_num))); + selected_origin.set(None); + } + None if is_mine => selected_origin.set(Some(field_num)), + None => {} + } + } + > + {field_num} + {move || { + let moves = staged_moves.get(); + let val = displayed_value(board, &moves, is_white, field_num); + let count = val.unsigned_abs(); + (count > 0).then(|| { + let color = if val > 0 { "white" } else { "black" }; + let display_n = (count as usize).min(4); + // outermost index: last for top rows, first for bottom rows. + let outer_idx = if is_top_row { display_n - 1 } else { 0 }; + let chips: Vec = (0..display_n).map(|i| { + let label = if i == outer_idx && count >= 5 { + count.to_string() + } else { + String::new() + }; + view! { +
{label}
+ }.into_any() + }).collect(); + view! {
{chips}
} + }) + }} +
+ } + .into_any() + }) + .collect() }; let (tl, tr, bl, br) = if is_white { @@ -113,15 +134,15 @@ pub fn Board( view! {
-
{fields_from(tl)}
+
{fields_from(tl, true)}
-
{fields_from(tr)}
+
{fields_from(tr, true)}
-
{fields_from(bl)}
+
{fields_from(bl, false)}
-
{fields_from(br)}
+
{fields_from(br, false)}
}