Compare commits
3 commits
bca03b7bcf
...
3c28eb465e
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c28eb465e | |||
| 77233b24c0 | |||
| 20e57eed2c |
5 changed files with 219 additions and 21 deletions
|
|
@ -38,5 +38,9 @@
|
||||||
"vs_bot_label": "vs Bot",
|
"vs_bot_label": "vs Bot",
|
||||||
"you_win": "You win!",
|
"you_win": "You win!",
|
||||||
"opp_wins": "{{ name }} wins!",
|
"opp_wins": "{{ name }} wins!",
|
||||||
"play_again": "Play again"
|
"play_again": "Play again",
|
||||||
|
"after_opponent_roll": "Opponent rolled",
|
||||||
|
"after_opponent_go": "Opponent chose to continue",
|
||||||
|
"after_opponent_move": "Opponent moved — your turn",
|
||||||
|
"continue_btn": "Continue"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,5 +38,9 @@
|
||||||
"vs_bot_label": "contre le bot",
|
"vs_bot_label": "contre le bot",
|
||||||
"you_win": "Vous avez gagné !",
|
"you_win": "Vous avez gagné !",
|
||||||
"opp_wins": "{{ name }} gagne !",
|
"opp_wins": "{{ name }} gagne !",
|
||||||
"play_again": "Rejouer"
|
"play_again": "Rejouer",
|
||||||
|
"after_opponent_roll": "L'adversaire a lancé les dés",
|
||||||
|
"after_opponent_go": "L'adversaire s'en va",
|
||||||
|
"after_opponent_move": "L'adversaire a joué — à vous",
|
||||||
|
"continue_btn": "Continuer"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ use crate::components::{ConnectingScreen, GameScreen, LoginScreen};
|
||||||
use crate::i18n::I18nContextProvider;
|
use crate::i18n::I18nContextProvider;
|
||||||
use crate::trictrac::backend::TrictracBackend;
|
use crate::trictrac::backend::TrictracBackend;
|
||||||
use crate::trictrac::bot_local::bot_decide;
|
use crate::trictrac::bot_local::bot_decide;
|
||||||
use crate::trictrac::types::{GameDelta, PlayerAction, ViewState};
|
use crate::trictrac::types::{GameDelta, PlayerAction, SerTurnStage, ViewState};
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
const RELAY_URL: &str = "ws://127.0.0.1:8080/ws";
|
const RELAY_URL: &str = "ws://127.0.0.1:8080/ws";
|
||||||
const GAME_ID: &str = "trictrac";
|
const GAME_ID: &str = "trictrac";
|
||||||
|
|
@ -26,6 +28,18 @@ pub struct GameUiState {
|
||||||
pub player_id: u16,
|
pub player_id: u16,
|
||||||
pub room_id: String,
|
pub room_id: String,
|
||||||
pub is_bot_game: bool,
|
pub is_bot_game: bool,
|
||||||
|
/// True when this state is a buffered snapshot awaiting player confirmation.
|
||||||
|
pub waiting_for_confirm: bool,
|
||||||
|
/// Why we are paused — drives the status-bar message in GameScreen.
|
||||||
|
pub pause_reason: Option<PauseReason>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reason the UI is paused waiting for the player to click Continue.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum PauseReason {
|
||||||
|
AfterOpponentRoll,
|
||||||
|
AfterOpponentGo,
|
||||||
|
AfterOpponentMove,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which screen is currently shown.
|
/// Which screen is currently shown.
|
||||||
|
|
@ -92,6 +106,8 @@ pub fn App() -> impl IntoView {
|
||||||
let screen = RwSignal::new(initial_screen);
|
let screen = RwSignal::new(initial_screen);
|
||||||
|
|
||||||
let (cmd_tx, mut cmd_rx) = mpsc::unbounded::<NetCommand>();
|
let (cmd_tx, mut cmd_rx) = mpsc::unbounded::<NetCommand>();
|
||||||
|
let pending: RwSignal<VecDeque<GameUiState>> = RwSignal::new(VecDeque::new());
|
||||||
|
provide_context(pending);
|
||||||
provide_context(cmd_tx.clone());
|
provide_context(cmd_tx.clone());
|
||||||
|
|
||||||
if let Some(s) = stored {
|
if let Some(s) = stored {
|
||||||
|
|
@ -171,9 +187,10 @@ pub fn App() -> impl IntoView {
|
||||||
|
|
||||||
if remote_config.is_none() {
|
if remote_config.is_none() {
|
||||||
loop {
|
loop {
|
||||||
let restart = run_local_bot_game(screen, &mut cmd_rx).await;
|
let restart = run_local_bot_game(screen, &mut cmd_rx, pending).await;
|
||||||
if !restart { break; }
|
if !restart { break; }
|
||||||
}
|
}
|
||||||
|
pending.update(|q| q.clear());
|
||||||
screen.set(Screen::Login { error: None });
|
screen.set(Screen::Login { error: None });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -219,12 +236,14 @@ pub fn App() -> impl IntoView {
|
||||||
_ => {
|
_ => {
|
||||||
clear_session();
|
clear_session();
|
||||||
session.disconnect();
|
session.disconnect();
|
||||||
|
pending.update(|q| q.clear());
|
||||||
screen.set(Screen::Login { error: None });
|
screen.set(Screen::Login { error: None });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
event = session.next_event().fuse() => match event {
|
event = session.next_event().fuse() => match event {
|
||||||
Some(SessionEvent::Update(u)) => {
|
Some(SessionEvent::Update(u)) => {
|
||||||
|
let prev_vs = vs.clone();
|
||||||
match u {
|
match u {
|
||||||
ViewStateUpdate::Full(state) => vs = state,
|
ViewStateUpdate::Full(state) => vs = state,
|
||||||
ViewStateUpdate::Incremental(delta) => vs.apply_delta(&delta),
|
ViewStateUpdate::Incremental(delta) => vs.apply_delta(&delta),
|
||||||
|
|
@ -239,18 +258,27 @@ pub fn App() -> impl IntoView {
|
||||||
view_state: Some(vs.clone()),
|
view_state: Some(vs.clone()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
screen.set(Screen::Playing(GameUiState {
|
push_or_show(
|
||||||
view_state: vs.clone(),
|
&prev_vs,
|
||||||
player_id,
|
GameUiState {
|
||||||
room_id: room_id_for_storage.clone(),
|
view_state: vs.clone(),
|
||||||
is_bot_game: false,
|
player_id,
|
||||||
}));
|
room_id: room_id_for_storage.clone(),
|
||||||
|
is_bot_game: false,
|
||||||
|
waiting_for_confirm: false,
|
||||||
|
pause_reason: None,
|
||||||
|
},
|
||||||
|
pending,
|
||||||
|
screen,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Some(SessionEvent::Disconnected(reason)) => {
|
Some(SessionEvent::Disconnected(reason)) => {
|
||||||
|
pending.update(|q| q.clear());
|
||||||
screen.set(Screen::Login { error: reason });
|
screen.set(Screen::Login { error: reason });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
pending.update(|q| q.clear());
|
||||||
screen.set(Screen::Login { error: None });
|
screen.set(Screen::Login { error: None });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -262,10 +290,17 @@ pub fn App() -> impl IntoView {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<I18nContextProvider>
|
<I18nContextProvider>
|
||||||
{move || match screen.get() {
|
{move || {
|
||||||
Screen::Login { error } => view! { <LoginScreen error=error /> }.into_any(),
|
let q = pending.get();
|
||||||
Screen::Connecting => view! { <ConnectingScreen /> }.into_any(),
|
if let Some(front) = q.front() {
|
||||||
Screen::Playing(state) => view! { <GameScreen state=state /> }.into_any(),
|
view! { <GameScreen state=front.clone() /> }.into_any()
|
||||||
|
} else {
|
||||||
|
match screen.get() {
|
||||||
|
Screen::Login { error } => view! { <LoginScreen error=error /> }.into_any(),
|
||||||
|
Screen::Connecting => view! { <ConnectingScreen /> }.into_any(),
|
||||||
|
Screen::Playing(state) => view! { <GameScreen state=state /> }.into_any(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
</I18nContextProvider>
|
</I18nContextProvider>
|
||||||
}
|
}
|
||||||
|
|
@ -275,6 +310,7 @@ pub fn App() -> impl IntoView {
|
||||||
async fn run_local_bot_game(
|
async fn run_local_bot_game(
|
||||||
screen: RwSignal<Screen>,
|
screen: RwSignal<Screen>,
|
||||||
cmd_rx: &mut futures::channel::mpsc::UnboundedReceiver<NetCommand>,
|
cmd_rx: &mut futures::channel::mpsc::UnboundedReceiver<NetCommand>,
|
||||||
|
pending: RwSignal<VecDeque<GameUiState>>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let mut backend = TrictracBackend::new(0);
|
let mut backend = TrictracBackend::new(0);
|
||||||
backend.player_arrival(0);
|
backend.player_arrival(0);
|
||||||
|
|
@ -298,14 +334,80 @@ async fn run_local_bot_game(
|
||||||
match bot_decide(backend.get_game()) {
|
match bot_decide(backend.get_game()) {
|
||||||
None => break,
|
None => break,
|
||||||
Some(action) => {
|
Some(action) => {
|
||||||
|
let prev_vs = vs.clone();
|
||||||
backend.inform_rpc(1, action);
|
backend.inform_rpc(1, action);
|
||||||
drain_and_update(&mut backend, &mut vs, screen);
|
for cmd in backend.drain_commands() {
|
||||||
|
if let BackendCommand::Delta(delta) = cmd {
|
||||||
|
vs.apply_delta(&delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push_or_show(
|
||||||
|
&prev_vs,
|
||||||
|
GameUiState {
|
||||||
|
view_state: vs.clone(),
|
||||||
|
player_id: 0,
|
||||||
|
room_id: String::new(),
|
||||||
|
is_bot_game: true,
|
||||||
|
waiting_for_confirm: false,
|
||||||
|
pause_reason: None,
|
||||||
|
},
|
||||||
|
pending,
|
||||||
|
screen,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Either queues the state as a buffered confirmation step (when the transition
|
||||||
|
/// warrants a pause) or shows it immediately. Always updates `screen` to the
|
||||||
|
/// live state so the UI falls through to the right content once pending drains.
|
||||||
|
fn push_or_show(
|
||||||
|
prev_vs: &ViewState,
|
||||||
|
new_state: GameUiState,
|
||||||
|
pending: RwSignal<VecDeque<GameUiState>>,
|
||||||
|
screen: RwSignal<Screen>,
|
||||||
|
) {
|
||||||
|
if let Some(reason) = infer_pause_reason(prev_vs, &new_state.view_state, new_state.player_id) {
|
||||||
|
pending.update(|q| {
|
||||||
|
q.push_back(GameUiState {
|
||||||
|
waiting_for_confirm: true,
|
||||||
|
pause_reason: Some(reason),
|
||||||
|
..new_state.clone()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
screen.set(Screen::Playing(new_state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares the previous and next ViewState to decide whether the transition
|
||||||
|
/// warrants a confirmation pause. Returns None when it is the local player's
|
||||||
|
/// own action (no pause needed).
|
||||||
|
fn infer_pause_reason(prev: &ViewState, next: &ViewState, player_id: u16) -> Option<PauseReason> {
|
||||||
|
let opponent_id = 1 - player_id;
|
||||||
|
|
||||||
|
if next.active_mp_player == Some(opponent_id) {
|
||||||
|
// Dice changed → opponent just rolled.
|
||||||
|
if next.dice != prev.dice {
|
||||||
|
return Some(PauseReason::AfterOpponentRoll);
|
||||||
|
}
|
||||||
|
// Was at HoldOrGoChoice, now Move, opponent still active → opponent went.
|
||||||
|
if prev.turn_stage == SerTurnStage::HoldOrGoChoice
|
||||||
|
&& next.turn_stage == SerTurnStage::Move
|
||||||
|
{
|
||||||
|
return Some(PauseReason::AfterOpponentGo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn switched to us → opponent moved.
|
||||||
|
if next.active_mp_player == Some(player_id) && prev.active_mp_player == Some(opponent_id) {
|
||||||
|
return Some(PauseReason::AfterOpponentMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn drain_and_update(
|
fn drain_and_update(
|
||||||
backend: &mut TrictracBackend,
|
backend: &mut TrictracBackend,
|
||||||
vs: &mut ViewState,
|
vs: &mut ViewState,
|
||||||
|
|
@ -327,5 +429,66 @@ fn drain_and_update(
|
||||||
player_id: 0,
|
player_id: 0,
|
||||||
room_id: String::new(),
|
room_id: String::new(),
|
||||||
is_bot_game: true,
|
is_bot_game: true,
|
||||||
|
waiting_for_confirm: false,
|
||||||
|
pause_reason: None,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::trictrac::types::{PlayerScore, SerStage, SerTurnStage};
|
||||||
|
|
||||||
|
fn score() -> PlayerScore {
|
||||||
|
PlayerScore { name: String::new(), points: 0, holes: 0, can_bredouille: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vs(dice: (u8, u8), turn_stage: SerTurnStage, active: Option<u16>) -> ViewState {
|
||||||
|
ViewState {
|
||||||
|
board: [0i8; 24],
|
||||||
|
stage: SerStage::InGame,
|
||||||
|
turn_stage,
|
||||||
|
active_mp_player: active,
|
||||||
|
scores: [score(), score()],
|
||||||
|
dice,
|
||||||
|
dice_jans: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dice_change_is_after_roll() {
|
||||||
|
let prev = vs((0, 0), SerTurnStage::RollDice, Some(1));
|
||||||
|
let next = vs((3, 5), SerTurnStage::Move, Some(1));
|
||||||
|
assert_eq!(infer_pause_reason(&prev, &next, 0), Some(PauseReason::AfterOpponentRoll));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hold_to_move_is_after_go() {
|
||||||
|
let prev = vs((3, 5), SerTurnStage::HoldOrGoChoice, Some(1));
|
||||||
|
let next = vs((3, 5), SerTurnStage::Move, Some(1));
|
||||||
|
assert_eq!(infer_pause_reason(&prev, &next, 0), Some(PauseReason::AfterOpponentGo));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn turn_switch_is_after_move() {
|
||||||
|
let prev = vs((3, 5), SerTurnStage::Move, Some(1));
|
||||||
|
let next = vs((3, 5), SerTurnStage::RollDice, Some(0));
|
||||||
|
assert_eq!(infer_pause_reason(&prev, &next, 0), Some(PauseReason::AfterOpponentMove));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn own_action_returns_none() {
|
||||||
|
let prev = vs((0, 0), SerTurnStage::RollDice, Some(0));
|
||||||
|
let next = vs((2, 4), SerTurnStage::Move, Some(0));
|
||||||
|
assert_eq!(infer_pause_reason(&prev, &next, 0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_active_player_returns_none() {
|
||||||
|
let mut prev = vs((0, 0), SerTurnStage::RollDice, None);
|
||||||
|
prev.stage = SerStage::PreGame;
|
||||||
|
let mut next = prev.clone();
|
||||||
|
next.active_mp_player = Some(0);
|
||||||
|
assert_eq!(infer_pause_reason(&prev, &next, 0), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use futures::channel::mpsc::UnboundedSender;
|
use futures::channel::mpsc::UnboundedSender;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, MoveRules};
|
use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, MoveRules};
|
||||||
|
|
||||||
use crate::app::{GameUiState, NetCommand};
|
use crate::app::{GameUiState, NetCommand, PauseReason};
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
use crate::trictrac::types::{JanEntry, PlayerAction, SerStage, SerTurnStage};
|
use crate::trictrac::types::{JanEntry, PlayerAction, SerStage, SerTurnStage};
|
||||||
|
|
||||||
|
|
@ -74,6 +76,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
vs.turn_stage,
|
vs.turn_stage,
|
||||||
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
||||||
);
|
);
|
||||||
|
let waiting_for_confirm = state.waiting_for_confirm;
|
||||||
|
let pause_reason = state.pause_reason.clone();
|
||||||
|
|
||||||
// ── 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![]);
|
||||||
|
|
@ -85,6 +89,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>>>()
|
||||||
|
.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();
|
||||||
|
|
@ -103,16 +109,29 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Auto-roll effect ─────────────────────────────────────────────────────
|
||||||
|
// GameScreen is fully re-mounted on every ViewState update (state is a
|
||||||
|
// plain prop, not a signal), so this effect fires exactly once per
|
||||||
|
// RollDice phase entry and will not double-send.
|
||||||
|
// Guard: suppressed while waiting_for_confirm — the AfterOpponentMove
|
||||||
|
// buffered state shows the human's RollDice turn but the auto-roll must
|
||||||
|
// wait until the buffer is drained and the live screen state is shown.
|
||||||
|
let show_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice;
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let dice = vs.dice;
|
let dice = vs.dice;
|
||||||
let show_dice = dice != (0, 0);
|
let show_dice = dice != (0, 0);
|
||||||
|
|
||||||
// ── Button senders ─────────────────────────────────────────────────────────
|
// ── Button senders ─────────────────────────────────────────────────────────
|
||||||
let cmd_tx_roll = cmd_tx.clone();
|
|
||||||
let cmd_tx_go = cmd_tx.clone();
|
let cmd_tx_go = cmd_tx.clone();
|
||||||
let cmd_tx_quit = cmd_tx.clone();
|
let cmd_tx_quit = cmd_tx.clone();
|
||||||
let cmd_tx_end_quit = cmd_tx.clone();
|
let cmd_tx_end_quit = cmd_tx.clone();
|
||||||
let cmd_tx_end_replay = cmd_tx.clone();
|
let cmd_tx_end_replay = cmd_tx.clone();
|
||||||
let show_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice;
|
|
||||||
let show_hold_go = is_my_turn && vs.turn_stage == SerTurnStage::HoldOrGoChoice;
|
let show_hold_go = is_my_turn && vs.turn_stage == SerTurnStage::HoldOrGoChoice;
|
||||||
|
|
||||||
// ── Valid move sequences for this turn ─────────────────────────────────────
|
// ── Valid move sequences for this turn ─────────────────────────────────────
|
||||||
|
|
@ -197,6 +216,13 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
// Status message
|
// Status message
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
<span>{move || {
|
<span>{move || {
|
||||||
|
if let Some(ref reason) = pause_reason {
|
||||||
|
return String::from(match reason {
|
||||||
|
PauseReason::AfterOpponentRoll => t_string!(i18n, after_opponent_roll),
|
||||||
|
PauseReason::AfterOpponentGo => t_string!(i18n, after_opponent_go),
|
||||||
|
PauseReason::AfterOpponentMove => t_string!(i18n, after_opponent_move),
|
||||||
|
});
|
||||||
|
}
|
||||||
let n = staged_moves.get().len();
|
let n = staged_moves.get().len();
|
||||||
if is_move_stage {
|
if is_move_stage {
|
||||||
t_string!(i18n, select_move, n = n + 1)
|
t_string!(i18n, select_move, n = n + 1)
|
||||||
|
|
@ -232,10 +258,10 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
{show_roll.then(|| view! {
|
{waiting_for_confirm.then(|| view! {
|
||||||
<button class="btn btn-primary" on:click=move |_| {
|
<button class="btn btn-primary" on:click=move |_| {
|
||||||
cmd_tx_roll.unbounded_send(NetCommand::Action(PlayerAction::Roll)).ok();
|
pending.update(|q| { q.pop_front(); });
|
||||||
}>{t!(i18n, roll_dice)}</button>
|
}>{t!(i18n, continue_btn)}</button>
|
||||||
})}
|
})}
|
||||||
{show_hold_go.then(|| view! {
|
{show_hold_go.then(|| view! {
|
||||||
<button class="btn btn-primary" on:click=move |_| {
|
<button class="btn btn-primary" on:click=move |_| {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ in
|
||||||
packages = [
|
packages = [
|
||||||
# for Leptos
|
# for Leptos
|
||||||
pkgs.trunk
|
pkgs.trunk
|
||||||
|
pkgs.lld
|
||||||
# pkgs.wasm-bindgen-cli_0_2_114
|
# pkgs.wasm-bindgen-cli_0_2_114
|
||||||
|
|
||||||
# pour burn-rs
|
# pour burn-rs
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue