fix(client_web): show opponent's dice animation

This commit is contained in:
Henri Bourcereau 2026-04-11 15:55:35 +02:00
parent 72c5e16ea3
commit 68ecafd0dc
3 changed files with 39 additions and 25 deletions

View file

@ -450,8 +450,8 @@ body {
strip after a few seconds, and reveal fully on hover. */ strip after a few seconds, and reveal fully on hover. */
.side-panel { .side-panel {
position: absolute; position: absolute;
right: 0; right: -8px;
top: 0; top: 10px;
z-index: 20; z-index: 20;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -645,8 +645,7 @@ body {
/* Wrapper: handles slide-in peek reveal lifecycle /* Wrapper: handles slide-in peek reveal lifecycle
The wrapper starts off-screen right (translateX(100%)), slides in on The wrapper starts off-screen right (translateX(100%)), slides in on
mount via animation, then Leptos adds .peeked after 3.4s to slide it mount via animation, then Leptos adds .peeked after 3.4s to slide it
back to a 28px peek strip. First hover adds .revealed for permanent back to a 28px peek strip. */
visibility. pointer-events: auto overrides the parent's none. */
@keyframes scoring-panel-enter { @keyframes scoring-panel-enter {
from { transform: translateX(100%); } from { transform: translateX(100%); }
to { transform: translateX(0); } to { transform: translateX(0); }
@ -662,7 +661,7 @@ body {
/* Peeked: slide right by the full panel width so the board is 100% clear. /* Peeked: slide right by the full panel width so the board is 100% clear.
The panel's left portion stays visible in whatever free space exists to The panel's left portion stays visible in whatever free space exists to
the right of the board (depends on viewport width). */ the right of the board. */
.scoring-panel-wrapper.peeked { .scoring-panel-wrapper.peeked {
transform: translateX(100%); transform: translateX(100%);
} }
@ -868,8 +867,8 @@ body {
.board-quarter .field.zone-retour:nth-child(odd) { --fc: #6a2810; } .board-quarter .field.zone-retour:nth-child(odd) { --fc: #6a2810; }
.board-quarter .field.zone-retour:nth-child(even) { --fc: #f2dfa0; } .board-quarter .field.zone-retour:nth-child(even) { --fc: #f2dfa0; }
/* ── Rest corner (§3) — before .clickable so green wins when interactive ── */ /* ── Rest corner — before .clickable so green wins when interactive ── */
.field.corner { --fc: var(--field-corner) !important; } /* .field.corner { --fc: var(--field-corner) !important; } */
/* Crown glyph sits behind checkers (z-index:-1) so it shows only on empty corners */ /* Crown glyph sits behind checkers (z-index:-1) so it shows only on empty corners */
.field.corner::after { .field.corner::after {

View file

@ -256,6 +256,7 @@ pub fn Board(
/// Whether we're in the move stage (determines used/unused die appearance). /// Whether we're in the move stage (determines used/unused die appearance).
#[prop(default = false)] #[prop(default = false)]
bar_is_move: bool, bar_is_move: bool,
#[prop(default = false)] is_my_turn: bool,
/// Whether the dice are a double (golden glow). /// Whether the dice are a double (golden glow).
#[prop(default = false)] #[prop(default = false)]
bar_is_double: bool, bar_is_double: bool,
@ -344,6 +345,9 @@ pub fn Board(
cls.push_str(" corner-available"); cls.push_str(" corner-available");
} }
} }
if is_rest_corner(field_num, !is_white) {
cls.push_str(" corner");
}
if all_in_exit && exit_field_test(field_num) { if all_in_exit && exit_field_test(field_num) {
cls.push_str(" exit-eligible"); cls.push_str(" exit-eligible");
} }
@ -501,8 +505,10 @@ pub fn Board(
let staged = staged_moves.get(); let staged = staged_moves.get();
let (u0, u1) = if bar_is_move { let (u0, u1) = if bar_is_move {
bar_matched_dice_used(&staged, dice_vals) bar_matched_dice_used(&staged, dice_vals)
} else { } else if is_my_turn {
(true, true) (true, true)
} else {
(false, false)
}; };
let used = if die_idx == 0 { u0 } else { u1 }; let used = if die_idx == 0 { u0 } else { u1 };
view! { <Die value=die_val used=used is_double=bar_is_double /> } view! { <Die value=die_val used=used is_double=bar_is_double /> }

View file

@ -37,8 +37,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
let cmd_tx = use_context::<UnboundedSender<NetCommand>>() let cmd_tx = use_context::<UnboundedSender<NetCommand>>()
.expect("UnboundedSender<NetCommand> not found in context"); .expect("UnboundedSender<NetCommand> not found in context");
let pending = use_context::<RwSignal<VecDeque<GameUiState>>>() let pending =
.expect("pending not found in context"); use_context::<RwSignal<VecDeque<GameUiState>>>().expect("pending not found in context");
let cmd_tx_effect = cmd_tx.clone(); let cmd_tx_effect = cmd_tx.clone();
Effect::new(move |_| { Effect::new(move |_| {
let moves = staged_moves.get(); let moves = staged_moves.get();
@ -68,7 +68,9 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
if show_roll && !waiting_for_confirm { if show_roll && !waiting_for_confirm {
let cmd_tx_auto = cmd_tx.clone(); let cmd_tx_auto = cmd_tx.clone();
Effect::new(move |_| { Effect::new(move |_| {
cmd_tx_auto.unbounded_send(NetCommand::Action(PlayerAction::Roll)).ok(); cmd_tx_auto
.unbounded_send(NetCommand::Action(PlayerAction::Roll))
.ok();
}); });
} }
@ -92,13 +94,19 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
let mut store_board = StoreBoard::new(); let mut store_board = StoreBoard::new();
store_board.set_positions(&Color::White, vs.board); store_board.set_positions(&Color::White, vs.board);
let store_dice = StoreDice { values: dice }; let store_dice = StoreDice { values: dice };
let color = if player_id == 0 { Color::White } else { Color::Black }; let color = if player_id == 0 {
Color::White
} else {
Color::Black
};
let rules = MoveRules::new(&color, &store_board, store_dice); let rules = MoveRules::new(&color, &store_board, store_dice);
let raw = rules.get_possible_moves_sequences(true, vec![]); let raw = rules.get_possible_moves_sequences(true, vec![]);
if player_id == 0 { if player_id == 0 {
raw raw
} else { } else {
raw.into_iter().map(|(m1, m2)| (m1.mirror(), m2.mirror())).collect() raw.into_iter()
.map(|(m1, m2)| (m1.mirror(), m2.mirror()))
.collect()
} }
} else { } else {
vec![] vec![]
@ -113,7 +121,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
// ── Scoring notifications ────────────────────────────────────────────────── // ── Scoring notifications ──────────────────────────────────────────────────
let my_scored_event = state.my_scored_event.clone(); let my_scored_event = state.my_scored_event.clone();
let opp_scored_event = state.opp_scored_event.clone(); let opp_scored_event = state.opp_scored_event.clone();
let hole_toast_info = my_scored_event.as_ref() let hole_toast_info = my_scored_event
.as_ref()
.filter(|e| e.holes_gained > 0) .filter(|e| e.holes_gained > 0)
.map(|e| (e.holes_total, e.bredouille)); .map(|e| (e.holes_total, e.bredouille));
@ -123,14 +132,16 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
// §6e — fields where a battue (hit) was scored; ripple animation shown there. // §6e — fields where a battue (hit) was scored; ripple animation shown there.
let hit_fields: Vec<u8> = { let hit_fields: Vec<u8> = {
let is_hit_jan = |jan: &Jan| matches!( let is_hit_jan = |jan: &Jan| {
jan, matches!(
Jan::TrueHitSmallJan jan,
| Jan::TrueHitBigJan Jan::TrueHitSmallJan
| Jan::TrueHitOpponentCorner | Jan::TrueHitBigJan
| Jan::FalseHitSmallJan | Jan::TrueHitOpponentCorner
| Jan::FalseHitBigJan | Jan::FalseHitSmallJan
); | Jan::FalseHitBigJan
)
};
let mut fields: Vec<u8> = vec![]; let mut fields: Vec<u8> = vec![];
for event_opt in [&my_scored_event, &opp_scored_event] { for event_opt in [&my_scored_event, &opp_scored_event] {
if let Some(event) = event_opt { if let Some(event) = event_opt {
@ -148,9 +159,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
} }
} }
} }
if !fields.is_empty() {
leptos::logging::log!("[6e] hit_fields = {:?}", fields);
}
fields fields
}; };
@ -248,6 +256,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
valid_sequences=valid_sequences valid_sequences=valid_sequences
bar_dice=show_dice.then_some(dice) bar_dice=show_dice.then_some(dice)
bar_is_move=is_move_stage bar_is_move=is_move_stage
is_my_turn=is_my_turn
bar_is_double=is_double_dice bar_is_double=is_double_dice
last_moves=last_moves last_moves=last_moves
hit_fields=hit_fields hit_fields=hit_fields