fix(web client): show dice animation & sound only once
This commit is contained in:
parent
9755ab1d41
commit
8f40304f41
4 changed files with 38 additions and 6 deletions
|
|
@ -45,6 +45,9 @@ pub struct GameUiState {
|
||||||
pub my_scored_event: Option<ScoredEvent>,
|
pub my_scored_event: Option<ScoredEvent>,
|
||||||
pub opp_scored_event: Option<ScoredEvent>,
|
pub opp_scored_event: Option<ScoredEvent>,
|
||||||
pub last_moves: Option<(CheckerMove, CheckerMove)>,
|
pub last_moves: Option<(CheckerMove, CheckerMove)>,
|
||||||
|
/// True on the echo screen state set alongside a pending item — suppresses dice
|
||||||
|
/// roll animation and sound since they already played on the pending screen.
|
||||||
|
pub suppress_dice_anim: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reason the UI is paused waiting for the player to click Continue.
|
/// Reason the UI is paused waiting for the player to click Continue.
|
||||||
|
|
@ -352,6 +355,7 @@ pub fn App() -> impl IntoView {
|
||||||
my_scored_event: None,
|
my_scored_event: None,
|
||||||
opp_scored_event: None,
|
opp_scored_event: None,
|
||||||
last_moves: compute_last_moves(&prev_vs, &vs, is_own_move),
|
last_moves: compute_last_moves(&prev_vs, &vs, is_own_move),
|
||||||
|
suppress_dice_anim: false,
|
||||||
},
|
},
|
||||||
pending,
|
pending,
|
||||||
screen,
|
screen,
|
||||||
|
|
@ -402,13 +406,16 @@ fn GameOverlay(
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let location = use_location();
|
let location = use_location();
|
||||||
|
|
||||||
|
// Memoize the front of the pending queue so that pushing a new item to the back
|
||||||
|
// does not re-mount GameScreen (and replay dice animation/sound) when the displayed
|
||||||
|
// state (the front) hasn't changed.
|
||||||
|
let pending_front = Memo::new(move |_| pending.with(|q| q.front().cloned()));
|
||||||
|
|
||||||
move || {
|
move || {
|
||||||
if location.pathname.get() != "/" {
|
if location.pathname.get() != "/" {
|
||||||
return view! {}.into_any();
|
return view! {}.into_any();
|
||||||
}
|
}
|
||||||
let q = pending.get();
|
if let Some(state) = pending_front.get() {
|
||||||
let front = q.front().cloned();
|
|
||||||
if let Some(state) = front {
|
|
||||||
return view! {
|
return view! {
|
||||||
<div class="game-overlay"><GameScreen state /></div>
|
<div class="game-overlay"><GameScreen state /></div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,9 @@ pub fn Board(
|
||||||
/// Fields where a hit (battue) was scored this turn — show ripple animation.
|
/// Fields where a hit (battue) was scored this turn — show ripple animation.
|
||||||
#[prop(default = vec![])]
|
#[prop(default = vec![])]
|
||||||
hit_fields: Vec<u8>,
|
hit_fields: Vec<u8>,
|
||||||
|
/// Suppress dice animation (echo screen shown after a pending confirm was dismissed).
|
||||||
|
#[prop(default = false)]
|
||||||
|
suppress_dice_anim: bool,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let board = view_state.board;
|
let board = view_state.board;
|
||||||
let white_points = view_state.scores[0].points;
|
let white_points = view_state.scores[0].points;
|
||||||
|
|
@ -283,6 +286,11 @@ pub fn Board(
|
||||||
view_state.turn_stage,
|
view_state.turn_stage,
|
||||||
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
||||||
);
|
);
|
||||||
|
// True when ANY player is in the Move/HoldOrGoChoice stage — i.e., dice are fresh for the active player.
|
||||||
|
let active_is_move_stage = matches!(
|
||||||
|
view_state.turn_stage,
|
||||||
|
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
||||||
|
);
|
||||||
let is_white = player_id == 0;
|
let is_white = player_id == 0;
|
||||||
let hovered_moves = use_context::<RwSignal<Vec<(CheckerMove, CheckerMove)>>>();
|
let hovered_moves = use_context::<RwSignal<Vec<(CheckerMove, CheckerMove)>>>();
|
||||||
|
|
||||||
|
|
@ -533,8 +541,13 @@ pub fn Board(
|
||||||
bar_matched_dice_used(&staged, dice_vals)
|
bar_matched_dice_used(&staged, dice_vals)
|
||||||
} else if is_my_turn {
|
} else if is_my_turn {
|
||||||
(true, true)
|
(true, true)
|
||||||
} else {
|
} else if active_is_move_stage && !suppress_dice_anim {
|
||||||
|
// Opponent has fresh dice in their Move stage (first view).
|
||||||
(false, false)
|
(false, false)
|
||||||
|
} else {
|
||||||
|
// Dice are old: either from the previous turn (opponent not yet
|
||||||
|
// rolled) or this is the echo screen after a pending confirm.
|
||||||
|
(true, true)
|
||||||
};
|
};
|
||||||
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 /> }
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
);
|
);
|
||||||
let waiting_for_confirm = state.waiting_for_confirm;
|
let waiting_for_confirm = state.waiting_for_confirm;
|
||||||
let pause_reason = state.pause_reason.clone();
|
let pause_reason = state.pause_reason.clone();
|
||||||
|
let suppress_dice_anim = state.suppress_dice_anim;
|
||||||
|
|
||||||
// ── Hovered jan moves (shown as arrows on the board) ──────────────────────
|
// ── Hovered jan moves (shown as arrows on the board) ──────────────────────
|
||||||
let hovered_jan_moves: RwSignal<Vec<(CheckerMove, CheckerMove)>> = RwSignal::new(vec![]);
|
let hovered_jan_moves: RwSignal<Vec<(CheckerMove, CheckerMove)>> = RwSignal::new(vec![]);
|
||||||
|
|
@ -206,8 +207,14 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Sound effects (fire once on mount = once per state snapshot) ──────────
|
// ── Sound effects (fire once on mount = once per state snapshot) ──────────
|
||||||
// Dice roll: dice just appeared (no preceding moves in this snapshot).
|
// Dice roll: dice are fresh for the currently active player (Move stage means
|
||||||
if show_dice && last_moves.is_none() {
|
// someone just rolled). Skipped on turn-switch states where the old dice linger
|
||||||
|
// in RollDice/MarkPoints stage before the opponent has rolled.
|
||||||
|
let active_is_move_stage = matches!(
|
||||||
|
vs.turn_stage,
|
||||||
|
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
||||||
|
);
|
||||||
|
if show_dice && last_moves.is_none() && active_is_move_stage && !suppress_dice_anim {
|
||||||
crate::game::sound::play_dice_roll();
|
crate::game::sound::play_dice_roll();
|
||||||
}
|
}
|
||||||
// Checker move: moves were committed in the preceding action.
|
// Checker move: moves were committed in the preceding action.
|
||||||
|
|
@ -328,6 +335,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
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
|
||||||
|
suppress_dice_anim=suppress_dice_anim
|
||||||
/>
|
/>
|
||||||
|
|
||||||
// ── Status, hints, and actions — cream strip below board ─
|
// ── Status, hints, and actions — cream strip below board ─
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ pub async fn run_local_bot_game(
|
||||||
my_scored_event: None,
|
my_scored_event: None,
|
||||||
opp_scored_event: None,
|
opp_scored_event: None,
|
||||||
last_moves: None,
|
last_moves: None,
|
||||||
|
suppress_dice_anim: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
|
@ -73,6 +74,7 @@ pub async fn run_local_bot_game(
|
||||||
my_scored_event: scored,
|
my_scored_event: scored,
|
||||||
opp_scored_event: opp_scored,
|
opp_scored_event: opp_scored,
|
||||||
last_moves: compute_last_moves(&prev_vs, &vs, true),
|
last_moves: compute_last_moves(&prev_vs, &vs, true),
|
||||||
|
suppress_dice_anim: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Some(NetCommand::PlayVsBot) => return true,
|
Some(NetCommand::PlayVsBot) => return true,
|
||||||
|
|
@ -102,6 +104,7 @@ pub async fn run_local_bot_game(
|
||||||
my_scored_event: None,
|
my_scored_event: None,
|
||||||
opp_scored_event: None,
|
opp_scored_event: None,
|
||||||
last_moves: compute_last_moves(&delta_prev_vs, &vs, false),
|
last_moves: compute_last_moves(&delta_prev_vs, &vs, false),
|
||||||
|
suppress_dice_anim: false,
|
||||||
},
|
},
|
||||||
pending,
|
pending,
|
||||||
screen,
|
screen,
|
||||||
|
|
@ -220,6 +223,7 @@ pub fn push_or_show(
|
||||||
});
|
});
|
||||||
screen.set(Screen::Playing(GameUiState {
|
screen.set(Screen::Playing(GameUiState {
|
||||||
last_moves: None,
|
last_moves: None,
|
||||||
|
suppress_dice_anim: true,
|
||||||
..new_state
|
..new_state
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue