use std::cell::Cell; use std::collections::VecDeque; use futures::channel::mpsc::UnboundedSender; use gloo_storage::Storage as _; use leptos::prelude::*; use trictrac_store::{ Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, Jan, MoveError, MoveRules, }; use super::board::{bar_matched_dice_used, Board}; use super::die::Die; use crate::app::{GameUiState, NetCommand, PauseReason}; use crate::game::trictrac::types::{PlayerAction, PreGameRollState, SerStage, SerTurnStage}; use crate::i18n::*; use crate::portal::lobby::{qr_svg, room_url}; use super::score_panel::MergedScorePanel; use super::scoring::ScoringPanel; #[component] pub fn GameScreen(state: GameUiState) -> impl IntoView { let i18n = use_i18n(); let vs = state.view_state.clone(); let vs_board = vs.board; let vs_dice = vs.dice; let player_id = state.player_id; let is_my_turn = vs.active_mp_player == Some(player_id); let is_move_stage = is_my_turn && matches!( vs.turn_stage, SerTurnStage::Move | SerTurnStage::HoldOrGoChoice ); let waiting_for_confirm = state.waiting_for_confirm; let pause_reason = state.pause_reason.clone(); let suppress_dice_anim = state.suppress_dice_anim; // ── Hovered jan moves (shown as arrows on the board) ────────────────────── let hovered_jan_moves: RwSignal> = RwSignal::new(vec![]); provide_context(hovered_jan_moves); // ── Staged move state ────────────────────────────────────────────────────── let selected_origin: RwSignal> = RwSignal::new(None); let staged_moves: RwSignal> = RwSignal::new(Vec::new()); let cmd_tx = use_context::>() .expect("UnboundedSender not found in context"); let pending = use_context::>>().expect("pending not found in context"); let cmd_tx_effect = cmd_tx.clone(); let prev_staged_len = Cell::new(0usize); // ── Free-play mode ───────────────────────────────────────────────────────── fn load_free_mode() -> bool { gloo_storage::LocalStorage::get::("trictrac_free_mode").unwrap_or(false) } fn save_free_mode(val: bool) { gloo_storage::LocalStorage::set("trictrac_free_mode", val).ok(); } let free_mode: RwSignal = RwSignal::new(load_free_mode()); let move_error: RwSignal>> = RwSignal::new(None); Effect::new(move |_| { let moves = staged_moves.get(); let n = moves.len(); if n > prev_staged_len.get() { crate::game::sound::play_checker_move(); } prev_staged_len.set(n); if n == 2 { let to_cm = |&(from, to): &(u8, u8)| { CheckerMove::new(from as usize, to as usize).unwrap_or_default() }; let m1 = to_cm(&moves[0]); let m2 = to_cm(&moves[1]); if free_mode.get_untracked() { let (vm1, vm2) = if player_id == 0 { (m1, m2) } else { (m1.mirror(), m2.mirror()) }; let mut store_board = StoreBoard::new(); store_board.set_positions(&Color::White, vs_board); let store_dice = StoreDice { values: vs_dice }; let color = if player_id == 0 { Color::White } else { Color::Black }; let rules = MoveRules::new(&color, &store_board, store_dice); if rules.moves_follow_rules(&(vm1, vm2)) { cmd_tx_effect .unbounded_send(NetCommand::Action(PlayerAction::Move(m1, m2))) .ok(); staged_moves.set(vec![]); selected_origin.set(None); prev_staged_len.set(0); } else { let specific_err = rules.moves_allowed(&(vm1, vm2)).err(); move_error.set(Some(specific_err)); // Keep staged_moves intact so pieces stay in place until Retry is clicked. } } else { cmd_tx_effect .unbounded_send(NetCommand::Action(PlayerAction::Move(m1, m2))) .ok(); staged_moves.set(vec![]); selected_origin.set(None); prev_staged_len.set(0); } } }); // ── Auto-roll effect ───────────────────────────────────────────────────── let show_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice && vs.stage != SerStage::PreGameRoll; 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 show_dice = dice != (0, 0); // ── Button senders ───────────────────────────────────────────────────────── let cmd_tx_go = cmd_tx.clone(); let cmd_tx_end_quit = cmd_tx.clone(); let cmd_tx_end_replay = cmd_tx.clone(); let show_hold_go = is_my_turn && vs.turn_stage == SerTurnStage::HoldOrGoChoice && state.my_scored_event.is_none(); // ── Valid move sequences for this turn ───────────────────────────────────── let valid_sequences: Vec<(CheckerMove, CheckerMove)> = if is_move_stage && dice != (0, 0) { 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 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() } } else { vec![] }; let valid_seqs_empty = valid_sequences.clone(); // ── Scores ───────────────────────────────────────────────────────────────── let my_score = vs.scores[player_id as usize].clone(); let opp_score = vs.scores[1 - player_id as usize].clone(); // ── Ceremony state ────────────────────────────────────────────────────────── let is_ceremony = vs.stage == SerStage::PreGameRoll; let pre_game_roll_data: Option = vs.pre_game_roll.clone(); let my_name_ceremony = my_score.name.clone(); let opp_name_ceremony = opp_score.name.clone(); let cmd_tx_ceremony = cmd_tx.clone(); // ── Scoring notifications ────────────────────────────────────────────────── let my_scored_event = state.my_scored_event.clone(); let opp_scored_event = state.opp_scored_event.clone(); let my_pts_earned: u8 = my_scored_event.as_ref().map_or(0, |e| { if e.holes_gained == 0 { e.points_earned } else { 0 } }); let opp_pts_earned: u8 = opp_scored_event.as_ref().map_or(0, |e| { if e.holes_gained == 0 { e.points_earned } else { 0 } }); let my_holes_gained_score: u8 = my_scored_event.as_ref().map_or(0, |e| e.holes_gained); let opp_holes_gained_score: u8 = opp_scored_event.as_ref().map_or(0, |e| e.holes_gained); let my_bredouille_flash: bool = my_scored_event .as_ref() .map_or(false, |e| e.bredouille && e.holes_gained > 0); let is_double_dice = dice.0 == dice.1 && dice.0 != 0; let last_moves = state.last_moves; let hit_fields: Vec = { let is_hit_jan = |jan: &Jan| { matches!( jan, Jan::TrueHitSmallJan | Jan::TrueHitBigJan | Jan::TrueHitOpponentCorner | Jan::FalseHitSmallJan | Jan::FalseHitBigJan ) }; let mut fields: Vec = vec![]; for event_opt in [&my_scored_event, &opp_scored_event] { if let Some(event) = event_opt { for entry in &event.jans { if is_hit_jan(&entry.jan) { for (m1, m2) in &entry.moves { for m in [m1, m2] { let to = m.get_to() as u8; if to != 0 && !fields.contains(&to) { fields.push(to); } } } } } } } fields }; // ── Sound effects ────────────────────────────────────────────────────────── 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(); } if last_moves.is_some() { crate::game::sound::play_checker_move(); } if let Some(ref ev) = my_scored_event { if ev.holes_gained > 0 { crate::game::sound::play_hole_scored(); } } if let Some(ref ev) = opp_scored_event { if ev.holes_gained > 0 { crate::game::sound::play_opp_hole_scored(); } } // ── Capture for closures ─────────────────────────────────────────────────── let stage = vs.stage.clone(); let turn_stage = vs.turn_stage.clone(); let turn_stage_for_panel = turn_stage.clone(); let turn_stage_for_sub = turn_stage.clone(); let room_id = state.room_id.clone(); let is_bot_game = state.is_bot_game; // ── Active player indicator ──────────────────────────────────────────────── let active_player_is_me: Option = if stage == SerStage::InGame { Some(is_my_turn) } else { None }; // ── Game-over info ───────────────────────────────────────────────────────── let stage_is_ended = stage == SerStage::Ended; let winner_is_me = my_score.holes >= 12; let my_name_end = my_score.name.clone(); let my_holes_end = my_score.holes; let opp_name_end = opp_score.name.clone(); let opp_holes_end = opp_score.holes; let share_url_copied = RwSignal::new(false); let share_url = if !is_bot_game { room_url(&room_id) } else { String::new() }; let share_svg = if !is_bot_game { qr_svg(&share_url) } else { String::new() }; view! {
// ── Share popover (while waiting for opponent) ─────────────────── {(!is_bot_game && stage == SerStage::PreGame).then(|| { let url_label = share_url.clone(); let url_copy = share_url.clone(); let svg = share_svg.clone(); view! { } }