//! The hand-end scoring summary screen. //! //! Shown when a hand finishes but the match continues: explains, in plain //! language, how each scoring category played out and how the running //! totals moved toward the target. Every player must acknowledge it before //! the next hand is dealt (or the server-side timeout deals anyway). use sycamore::prelude::*; use crate::components::card::{card_back, card_img}; use crate::model::{GameView, HandSummary, Scores}; use crate::ws::GameSocket; /// One explanatory row: icon, title, plain-language sentence, points chip. fn award_row( icon: View, title: &'static str, text: String, winner: Option, points: &'static str, ) -> View { let cls = match winner.as_deref() { Some("A") => "score-row team-a", Some("B") => "score-row team-b", _ => "score-row tie", }; let chip = match &winner { Some(t) => format!("Team {t} {points}"), None => "tie".to_string(), }; 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) } } } } /// Winner's count first, then the loser's, for a natural sentence. fn winner_first(a: i32, b: i32, winner: Option<&String>) -> (i32, i32) { match winner.map(String::as_str) { Some("B") => (b, a), _ => (a, b), } } /// The five scoring rows of a completed hand. pub fn summary_rows(summary: HandSummary) -> View { let rows: Vec = vec![ // Carte award_row( card_back("score-mini"), "Carte", match &summary.award.carte { Some(t) => { let (w, l) = winner_first(summary.cards.a, summary.cards.b, Some(t)); format!("Team {t} captured more cards ({w} vs {l})") } None => format!( "Both teams captured {} cards — no point", summary.cards.a ), }, summary.award.carte.clone(), "+1", ), // Denara award_row( card_img("02D".to_string(), "score-mini"), "Denari", match &summary.award.denara { Some(t) => { let (w, l) = winner_first(summary.denara.a, summary.denara.b, Some(t)); format!("Team {t} collected more denari cards ({w} vs {l})") } None => format!( "Both teams collected {} denari cards — no point", summary.denara.a ), }, summary.award.denara.clone(), "+1", ), // Settebello award_row( card_img("07D".to_string(), "score-mini"), "Settebello", match &summary.award.settebello { Some(t) => format!("Team {t} captured the 7 of denari — the Settebello"), None => "Nobody captured the Settebello".to_string(), }, summary.award.settebello.clone(), "+1", ), // Primiera award_row( card_img("10D".to_string(), "score-mini"), "Primiera", match &summary.award.primiera { Some(t) => { let (w, l) = winner_first(summary.primiera.a, summary.primiera.b, Some(t)); format!("Team {t} holds the strongest primiera ({w} vs {l})") } None => format!( "Both primiere are worth {} — no point", summary.primiera.a ), }, summary.award.primiera.clone(), "+1", ), // Napola (only when the rule is enabled for this match) match summary.napola { None => view! {}, Some(napola) => { let n = match summary.award.napola.as_deref() { Some("A") => napola.a, Some("B") => napola.b, _ => 0, }; let text = match (&summary.award.napola, n) { (Some(t), 10) => format!( "Team {t} swept the whole denari suit — napola! Instant match win" ), (Some(t), n) => format!( "Team {t} captured {n} consecutive denari from the ace" ), (None, _) => "No napola this hand".to_string(), }; let chip = match &summary.award.napola { Some(t) => format!("Team {t} +{n}"), None => "tie".to_string(), }; let cls = match summary.award.napola.as_deref() { Some("A") => "score-row team-a", Some("B") => "score-row team-b", _ => "score-row tie", }; view! { div(class=cls) { div(class="score-icon") { (card_img("01D".to_string(), "score-mini")) } div(class="score-body") { div(class="score-title") { "Napola" } div(class="score-text") { (text) } } div(class="score-points") { (chip) } } } } }, // Scope { let a = summary.scope.a; let b = summary.scope.b; let text = if a == 0 && b == 0 { "No scope this hand".to_string() } else { format!("Team A made {a} scope · Team B made {b} scope") }; let chip = format!("+{a} · +{b}"); view! { div(class="score-row scope-row") { div(class="score-icon") { (card_back("score-mini")) } div(class="score-body") { div(class="score-title") { "Scope" } div(class="score-text") { (text) } } div(class="score-points") { (chip) } } } }, ]; view! { div(class="score-rows") { (rows) } } } /// Running totals with progress toward the target score, including the /// points gained in the hand just played. pub fn totals(game: &GameView) -> View { let scores = game.scores.unwrap_or(Scores { a: 0, b: 0 }); let target = game.target_score.max(1); let pct_a = (100 * scores.a / target).min(100); let pct_b = (100 * scores.b / target).min(100); let (gained_a, gained_b) = game .last_hand .as_ref() .map(|s| (s.team_a_points, s.team_b_points)) .unwrap_or((0, 0)); view! { div(class="totals") { div(class="total-row team-a") { span(class="total-label") { "Team A" } div(class="progress") { div(class="progress-fill", style=format!("width: {pct_a}%")) {} } span(class="total-value") { (scores.a) " / " (target) span(class="gained") { "+" (gained_a) } } } div(class="total-row team-b") { span(class="total-label") { "Team B" } div(class="progress") { div(class="progress-fill", style=format!("width: {pct_b}%")) {} } span(class="total-value") { (scores.b) " / " (target) span(class="gained") { "+" (gained_b) } } } } } } /// The full hand-end modal: explanation + totals + acknowledgement button. pub fn hand_summary_modal( game: GameView, socket: Signal>, now: Signal, ) -> View { let Some(summary) = game.last_hand.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 rows = summary_rows(summary.clone()); let totals_view = totals(&game); let title = format!("Hand {} — results", summary.hand); let action = if game.spectator { // Spectators cannot acknowledge; they just watch the summary. view! { p(class="text-secondary small mb-0") { "Spectating — the next hand 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 hand" } } }; view! { div(class="overlay") { div(class="picker summary-panel") { h2 { (title) } (rows) (totals_view) div(class="summary-actions") { (action) } (countdown) } } } }