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. */
.side-panel {
position: absolute;
right: 0;
top: 0;
right: -8px;
top: 10px;
z-index: 20;
display: flex;
flex-direction: column;
@ -645,8 +645,7 @@ body {
/* Wrapper: handles slide-in peek reveal lifecycle
The wrapper starts off-screen right (translateX(100%)), slides in on
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
visibility. pointer-events: auto overrides the parent's none. */
back to a 28px peek strip. */
@keyframes scoring-panel-enter {
from { transform: translateX(100%); }
to { transform: translateX(0); }
@ -662,7 +661,7 @@ body {
/* 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 right of the board (depends on viewport width). */
the right of the board. */
.scoring-panel-wrapper.peeked {
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(even) { --fc: #f2dfa0; }
/* ── Rest corner (§3) — before .clickable so green wins when interactive ── */
.field.corner { --fc: var(--field-corner) !important; }
/* ── Rest corner — before .clickable so green wins when interactive ── */
/* .field.corner { --fc: var(--field-corner) !important; } */
/* Crown glyph sits behind checkers (z-index:-1) so it shows only on empty corners */
.field.corner::after {

View file

@ -256,6 +256,7 @@ pub fn Board(
/// Whether we're in the move stage (determines used/unused die appearance).
#[prop(default = false)]
bar_is_move: bool,
#[prop(default = false)] is_my_turn: bool,
/// Whether the dice are a double (golden glow).
#[prop(default = false)]
bar_is_double: bool,
@ -344,6 +345,9 @@ pub fn Board(
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) {
cls.push_str(" exit-eligible");
}
@ -501,8 +505,10 @@ pub fn Board(
let staged = staged_moves.get();
let (u0, u1) = if bar_is_move {
bar_matched_dice_used(&staged, dice_vals)
} else {
} else if is_my_turn {
(true, true)
} else {
(false, false)
};
let used = if die_idx == 0 { u0 } else { u1 };
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>>()
.expect("UnboundedSender<NetCommand> not found in context");
let pending = use_context::<RwSignal<VecDeque<GameUiState>>>()
.expect("pending not found in context");
let pending =
use_context::<RwSignal<VecDeque<GameUiState>>>().expect("pending not found in context");
let cmd_tx_effect = cmd_tx.clone();
Effect::new(move |_| {
let moves = staged_moves.get();
@ -68,7 +68,9 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
if show_roll && !waiting_for_confirm {
let cmd_tx_auto = cmd_tx.clone();
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();
store_board.set_positions(&Color::White, vs.board);
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 raw = rules.get_possible_moves_sequences(true, vec![]);
if player_id == 0 {
raw
} else {
raw.into_iter().map(|(m1, m2)| (m1.mirror(), m2.mirror())).collect()
raw.into_iter()
.map(|(m1, m2)| (m1.mirror(), m2.mirror()))
.collect()
}
} else {
vec![]
@ -113,7 +121,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
// ── Scoring notifications ──────────────────────────────────────────────────
let my_scored_event = state.my_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)
.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.
let hit_fields: Vec<u8> = {
let is_hit_jan = |jan: &Jan| matches!(
jan,
Jan::TrueHitSmallJan
| Jan::TrueHitBigJan
| Jan::TrueHitOpponentCorner
| Jan::FalseHitSmallJan
| Jan::FalseHitBigJan
);
let is_hit_jan = |jan: &Jan| {
matches!(
jan,
Jan::TrueHitSmallJan
| Jan::TrueHitBigJan
| Jan::TrueHitOpponentCorner
| Jan::FalseHitSmallJan
| Jan::FalseHitBigJan
)
};
let mut fields: Vec<u8> = vec![];
for event_opt in [&my_scored_event, &opp_scored_event] {
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
};
@ -248,6 +256,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
valid_sequences=valid_sequences
bar_dice=show_dice.then_some(dice)
bar_is_move=is_move_stage
is_my_turn=is_my_turn
bar_is_double=is_double_dice
last_moves=last_moves
hit_fields=hit_fields