//! The briscola deal-end summary screen. //! //! Shown when a deal finishes but the match continues: the points won in //! the deal just played, the running totals toward the target and the //! acknowledgement button (the server deals on when everyone is ready, or //! after the hand-end timeout). use sycamore::prelude::*; use crate::components::card::{card_back, card_img}; use crate::model::{card_label, is_chiamata, is_individual_game, DealSummary, GameView}; use crate::ws::GameSocket; /// Names of the two sides: the caller and partner for briscola chiamata, /// the players' names for games without partnerships (classic two-player /// briscola), "Team A"/"Team B" otherwise. fn side_names(game: &GameView) -> (String, String) { if is_chiamata(&game.game_type) { game.chiamata_sides() } else if is_individual_game(&game.game_type) { ( game.player_name_for_team("A"), game.player_name_for_team("B"), ) } else { ("Team A".to_string(), "Team B".to_string()) } } /// One explanatory row: icon, title, plain-language sentence, points chip. fn summary_row( icon: View, title: &'static str, text: String, cls: &'static str, chip: String, ) -> View { view! { div(class=cls) { div(class="score-icon") { (icon) } div(class="score-body") { div(class="score-title") { (title) } div(class="score-text") { (text) } } div(class="score-points") { (chip) } } } } /// The scoring rows of one completed deal (also used by the final screen). pub fn deal_rows(game: &GameView, summary: &DealSummary) -> View { let a = summary.points.a; let b = summary.points.b; let (name_a, name_b) = side_names(game); let cls = if summary.patta { "score-row tie" } else if a > b { "score-row team-a" } else { "score-row team-b" }; let chip = if summary.patta { "patta".to_string() } else { format!("+{a} · +{b}") }; let patta_note = summary.patta.then(|| view! { p(class="text-warning small mb-0") { "60–60: patta — the deal is replayed and its points do not count." } }); let called_row = if is_chiamata(&game.game_type) { game.called_card.clone().map(|code| { let label = card_label(&code); let text = if game.solo { format!("{name_a} called {label} in hand and plays alone.") } else { format!("{label}: its holder is the secret partner; its suit is trump.") }; summary_row( card_img(code, "score-mini"), "Chiamata", text, "score-row chiamata-row", label, ) }) } else { None }; let points_row = summary_row( card_img("01D".to_string(), "score-mini"), "Deal points", format!("{name_a} {a} · {name_b} {b}"), cls, chip, ); let cards = summary.cards.map(|c| { summary_row( card_back("score-mini"), "Cards captured", format!("{name_a} {} · {name_b} {}", c.a, c.b), "score-row", format!("{} · {}", c.a, c.b), ) }); view! { div(class="score-rows") { (called_row) (points_row) (patta_note) (cards) } } } /// Running totals with progress toward the target, including the points /// gained in the deal just played (zero for a patta). fn totals(game: &GameView, summary: &DealSummary) -> View { // Briscola chiamata plays a single deal: there is no running total. let Some(running) = summary.totals else { return view! {}; }; let target = game.target_score.max(1); let pct_a = (100 * running.a / target).min(100); let pct_b = (100 * running.b / target).min(100); let (gained_a, gained_b) = if summary.patta { (0, 0) } else { (summary.points.a, summary.points.b) }; let (name_a, name_b) = side_names(game); view! { div(class="totals") { div(class="total-row team-a") { span(class="total-label") { (name_a) } div(class="progress") { div(class="progress-fill", style=format!("width: {pct_a}%")) {} } span(class="total-value") { (running.a) " / " (target) span(class="gained") { "+" (gained_a) } } } div(class="total-row team-b") { span(class="total-label") { (name_b) } div(class="progress") { div(class="progress-fill", style=format!("width: {pct_b}%")) {} } span(class="total-value") { (running.b) " / " (target) span(class="gained") { "+" (gained_b) } } } } } } /// The full deal-end modal: points + totals + acknowledgement button. pub fn deal_summary_modal( game: GameView, socket: Signal>, now: Signal, ) -> View { let Some(summary) = game.last_deal.clone() else { return view! {}; }; let viewer_seat = game .players .iter() .find(|p| p.hand.is_some()) .map(|p| p.seat); let acked = game.acknowledged.clone(); let already_acked = viewer_seat.is_some_and(|s| acked.contains(&s)); let waiting: Vec = game .players .iter() .filter(|p| !acked.contains(&p.seat)) .map(|p| p.name.clone()) .collect(); let countdown = game.hand_end_deadline.as_ref().map(|deadline| { // A dynamic closure so only the ticking number re-renders, not the // whole modal (which would swap DOM nodes under the user's cursor). let deadline_ms = js_sys::Date::parse(deadline); view! { p(class="text-secondary small") { "Auto-continuing in " (move || { ((deadline_ms - now.get_clone()) / 1000.0).ceil().max(0.0) as i32 }) "s" } } }); let title = format!("Deal {} — results", summary.deal); let rows = deal_rows(&game, &summary); let totals_view = totals(&game, &summary); let action = if game.spectator { // Spectators cannot acknowledge; they just watch the summary. view! { p(class="text-secondary small mb-0") { "Spectating — the next deal is dealt when every player is ready." } } } else if already_acked { let waiting_text = format!("Waiting for {}…", waiting.join(", ")); view! { button(class="btn btn-warning", disabled=true) { (waiting_text) } } } else { view! { button(class="btn btn-warning", on:click=move |_| { if let Some(s) = socket.get_clone() { s.ack(); } }) { "Understood, next deal" } } }; view! { div(class="overlay") { div(class="picker summary-panel") { h2 { (title) } (rows) (totals_view) div(class="summary-actions") { (action) } (countdown) } } } }