feat(web client): responsive design
This commit is contained in:
parent
9db942354c
commit
576f9706c2
8 changed files with 3911 additions and 364 deletions
|
|
@ -1291,7 +1291,7 @@ a:hover { text-decoration: underline; }
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
animation: scoring-panel-enter 0.3s ease-out;
|
animation: scoring-panel-enter 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
@ -1889,8 +1889,7 @@ a:hover { text-decoration: underline; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.free-mode-error {
|
.free-mode-error {
|
||||||
display: flex;
|
text-align: center;
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
background: rgba(180, 60, 30, 0.12);
|
background: rgba(180, 60, 30, 0.12);
|
||||||
border: 1px solid rgba(180, 60, 30, 0.4);
|
border: 1px solid rgba(180, 60, 30, 0.4);
|
||||||
|
|
@ -1900,7 +1899,6 @@ a:hover { text-decoration: underline; }
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.free-mode-error-msg {
|
.free-mode-error-msg {
|
||||||
flex: 1;
|
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #8b2000;
|
color: #8b2000;
|
||||||
|
|
@ -2303,3 +2301,219 @@ a:hover { text-decoration: underline; }
|
||||||
background: rgba(200,164,72,0.1);
|
background: rgba(200,164,72,0.1);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Prevent horizontal scrollbar from the full-bleed strip */
|
||||||
|
.game-overlay { overflow-x: hidden !important; }
|
||||||
|
|
||||||
|
/* Board bar: hide die slots, keep the rail as a thin divider */
|
||||||
|
.bar-die-slot { display: none !important; }
|
||||||
|
.board-bar { width: 5px; overflow: hidden; }
|
||||||
|
|
||||||
|
/* ── Full-width in-flow player strip ─────────────────────────────────── */
|
||||||
|
.players-strip {
|
||||||
|
width: 100vw;
|
||||||
|
margin-top: -1.5rem; /* undo game-overlay top padding */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--ui-parchment);
|
||||||
|
border-bottom: 2px solid var(--ui-gold-dark);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||||
|
padding: 0.35rem 1.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-player { display: flex; align-items: center; flex: 1; min-width: 0; }
|
||||||
|
.strip-player-left { justify-content: flex-end; }
|
||||||
|
.strip-player-right { justify-content: flex-start; }
|
||||||
|
|
||||||
|
.strip-active-zone {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.28rem 0.5rem;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.strip-active-zone.active { background: rgba(58,42,10,0.08); }
|
||||||
|
|
||||||
|
/* Checker-style circles */
|
||||||
|
.strip-avatar {
|
||||||
|
width: 38px; height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.strip-avatar-me {
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse 50% 35% at 36% 30%, rgba(255,255,255,0.65) 0%, transparent 100%),
|
||||||
|
radial-gradient(circle, transparent 68%, rgba(160,130,70,0.22) 68.5%, rgba(160,130,70,0.22) 71.5%, transparent 72%),
|
||||||
|
radial-gradient(circle, transparent 43%, rgba(160,130,70,0.17) 43.5%, rgba(160,130,70,0.17) 46.5%, transparent 47%),
|
||||||
|
radial-gradient(circle at 38% 32%, #ffffff 0%, var(--checker-white) 52%, #c0b288 100%);
|
||||||
|
border: 1.8px solid var(--checker-ring);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.35), inset 0 -1px 3px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.strip-avatar-opp {
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse 40% 28% at 36% 30%, rgba(110,65,30,0.38) 0%, transparent 100%),
|
||||||
|
radial-gradient(circle, transparent 68%, rgba(200,164,72,0.18) 68.5%, rgba(200,164,72,0.18) 71.5%, transparent 72%),
|
||||||
|
radial-gradient(circle, transparent 43%, rgba(200,164,72,0.13) 43.5%, rgba(200,164,72,0.13) 46.5%, transparent 47%),
|
||||||
|
radial-gradient(circle at 38% 32%, #4a2e1a 0%, #1c1008 45%, var(--checker-black) 100%);
|
||||||
|
border: 1.8px solid var(--checker-ring);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.5), inset 0 -1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strip peg overrides */
|
||||||
|
.players-strip .peg-track { gap: 3px; }
|
||||||
|
.players-strip .peg-hole { width: 12px; height: 12px; }
|
||||||
|
.players-strip .peg-hole.filled {
|
||||||
|
background: #5aab38; border-color: #3a7828;
|
||||||
|
box-shadow: 0 0 5px rgba(90,171,56,0.55);
|
||||||
|
}
|
||||||
|
.players-strip .peg-hole.peg-opp.filled {
|
||||||
|
background: #c05030; border-color: #8a3018;
|
||||||
|
box-shadow: 0 0 5px rgba(192,80,48,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strip score-row-name: remove fixed width from v01 */
|
||||||
|
.players-strip .score-row-name { width: auto; }
|
||||||
|
|
||||||
|
/* No ghost bar below pts-counter in the strip */
|
||||||
|
.players-strip .pts-counter-wrap { padding-bottom: 0; }
|
||||||
|
|
||||||
|
/* Center "Trictrac" title */
|
||||||
|
.players-strip-center {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 1rem;
|
||||||
|
border-left: 1px solid rgba(138,106,40,0.2);
|
||||||
|
border-right: 1px solid rgba(138,106,40,0.2);
|
||||||
|
}
|
||||||
|
.strip-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--ui-ink);
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Body: board + controls ──────────────────────────────────────────── */
|
||||||
|
.main-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Controls column (sidebar on wide, row on narrow) ────────────────── */
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
@media (min-width: 920px) {
|
||||||
|
.controls {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-dice {
|
||||||
|
background: var(--board-rail);
|
||||||
|
border-radius: 5px;
|
||||||
|
border-top: 2px solid var(--ui-gold-dark);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||||
|
padding: 0.6rem 0.75rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ctrl-dice-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.55rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Free-mode toggle: light text on dark board-rail background */
|
||||||
|
.ctrl-dice .free-mode-toggle {
|
||||||
|
color: var(--ui-parchment);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
.ctrl-dice .free-mode-help {
|
||||||
|
border-color: rgba(242,232,208,0.35);
|
||||||
|
color: rgba(242,232,208,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-status {
|
||||||
|
background: var(--ui-parchment);
|
||||||
|
border-radius: 5px;
|
||||||
|
border-top: 2px solid var(--ui-gold-dark);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||||
|
padding: 0.65rem 0.75rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.ctrl-status .game-status {
|
||||||
|
color: var(--ui-ink);
|
||||||
|
text-shadow: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.ctrl-status .board-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.ctrl-status .game-sub-prompt {
|
||||||
|
color: #887766;
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.67rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoring-row .scoring-panels-container {
|
||||||
|
position: static;
|
||||||
|
top: auto; left: auto; right: auto; bottom: auto;
|
||||||
|
z-index: auto;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.scoring-row .scoring-panel {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive: ≤919px → controls becomes a bottom bar ─────────────── */
|
||||||
|
@media (max-width: 919px) {
|
||||||
|
.main-body {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ctrl-status { flex: 1; }
|
||||||
|
/* Hide pegs on small screens to save space in the strip */
|
||||||
|
.players-strip .peg-track { display: none; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ fn field_zone_class(field_num: u8) -> &'static str {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns (d0_used, d1_used) for the bar dice display.
|
/// Returns (d0_used, d1_used) for the bar dice display.
|
||||||
fn bar_matched_dice_used(staged: &[(u8, u8)], dice: (u8, u8)) -> (bool, bool) {
|
pub(crate) fn bar_matched_dice_used(staged: &[(u8, u8)], dice: (u8, u8)) -> (bool, bool) {
|
||||||
let mut d0 = false;
|
let mut d0 = false;
|
||||||
let mut d1 = false;
|
let mut d1 = false;
|
||||||
for &(from, to) in staged {
|
for &(from, to) in staged {
|
||||||
|
|
@ -251,7 +251,11 @@ fn free_mode_origins_for(board: [i8; 24], staged: &[(u8, u8)], is_white: bool) -
|
||||||
(1u8..=24)
|
(1u8..=24)
|
||||||
.filter(|&f| {
|
.filter(|&f| {
|
||||||
let v = displayed_value(board, staged, is_white, f);
|
let v = displayed_value(board, staged, is_white, f);
|
||||||
if is_white { v > 0 } else { v < 0 }
|
if is_white {
|
||||||
|
v > 0
|
||||||
|
} else {
|
||||||
|
v < 0
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +282,11 @@ fn free_mode_dests_for(
|
||||||
let &(f0, t0) = &staged[0];
|
let &(f0, t0) = &staged[0];
|
||||||
if t0 == 0 {
|
if t0 == 0 {
|
||||||
// First move was an exit — can't reliably infer die, offer both
|
// First move was an exit — can't reliably infer die, offer both
|
||||||
if dice.0 == dice.1 { vec![dice.0] } else { vec![dice.0, dice.1] }
|
if dice.0 == dice.1 {
|
||||||
|
vec![dice.0]
|
||||||
|
} else {
|
||||||
|
vec![dice.0, dice.1]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let dist: u8 = if is_white {
|
let dist: u8 = if is_white {
|
||||||
t0.saturating_sub(f0)
|
t0.saturating_sub(f0)
|
||||||
|
|
@ -299,7 +307,11 @@ fn free_mode_dests_for(
|
||||||
|
|
||||||
let opp_present = |f: u8| -> bool {
|
let opp_present = |f: u8| -> bool {
|
||||||
let v = displayed_value(board, staged, is_white, f);
|
let v = displayed_value(board, staged, is_white, f);
|
||||||
if is_white { v < 0 } else { v > 0 }
|
if is_white {
|
||||||
|
v < 0
|
||||||
|
} else {
|
||||||
|
v > 0
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut dests = vec![];
|
let mut dests = vec![];
|
||||||
|
|
@ -676,23 +688,11 @@ pub fn Board(
|
||||||
(&TOP_LEFT_B, &TOP_RIGHT_B, &BOT_LEFT_B, &BOT_RIGHT_B)
|
(&TOP_LEFT_B, &TOP_RIGHT_B, &BOT_LEFT_B, &BOT_RIGHT_B)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Zone label pairs (top-left, top-right, bot-left, bot-right) per perspective.
|
|
||||||
let (label_tl, label_tr, label_bl, label_br) = if is_white {
|
|
||||||
("", "jan de retour", "grand jan", "petit jan")
|
|
||||||
} else {
|
|
||||||
("petit jan", "grand jan", "jan de retour", "")
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
// board-wrapper keeps zone labels outside .board so the SVG overlay
|
// board-wrapper keeps zone labels outside .board so the SVG overlay
|
||||||
// inside .board stays correctly positioned (position:absolute top:0 left:0
|
// inside .board stays correctly positioned (position:absolute top:0 left:0
|
||||||
// is relative to .board, not the wrapper).
|
// is relative to .board, not the wrapper).
|
||||||
<div class="board-wrapper">
|
<div class="board-wrapper">
|
||||||
<div class="zone-labels-row">
|
|
||||||
<div class="zone-label zone-label-quarter">{label_tl}</div>
|
|
||||||
<div class="zone-label zone-label-bar"></div>
|
|
||||||
<div class="zone-label zone-label-quarter">{label_tr}</div>
|
|
||||||
</div>
|
|
||||||
<div class="board">
|
<div class="board">
|
||||||
<div class="board-row top-row">
|
<div class="board-row top-row">
|
||||||
<div class="board-quarter">{fields_from(tl, true)}</div>
|
<div class="board-quarter">{fields_from(tl, true)}</div>
|
||||||
|
|
@ -833,11 +833,6 @@ pub fn Board(
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="zone-labels-row">
|
|
||||||
<div class="zone-label zone-label-quarter">{label_bl}</div>
|
|
||||||
<div class="zone-label zone-label-bar"></div>
|
|
||||||
<div class="zone-label zone-label-quarter">{label_br}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,17 @@ use std::collections::VecDeque;
|
||||||
use futures::channel::mpsc::UnboundedSender;
|
use futures::channel::mpsc::UnboundedSender;
|
||||||
use gloo_storage::Storage as _;
|
use gloo_storage::Storage as _;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, Jan, MoveError, MoveRules};
|
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 super::die::Die;
|
||||||
use crate::app::{GameUiState, NetCommand, PauseReason};
|
use crate::app::{GameUiState, NetCommand, PauseReason};
|
||||||
use crate::game::trictrac::types::{PlayerAction, PreGameRollState, SerStage, SerTurnStage};
|
use crate::game::trictrac::types::{PlayerAction, PreGameRollState, SerStage, SerTurnStage};
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
use crate::portal::lobby::{qr_svg, room_url};
|
use crate::portal::lobby::{qr_svg, room_url};
|
||||||
|
|
||||||
use super::board::Board;
|
|
||||||
use super::score_panel::MergedScorePanel;
|
use super::score_panel::MergedScorePanel;
|
||||||
use super::scoring::ScoringPanel;
|
use super::scoring::ScoringPanel;
|
||||||
|
|
||||||
|
|
@ -47,14 +49,9 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let pending =
|
let pending =
|
||||||
use_context::<RwSignal<VecDeque<GameUiState>>>().expect("pending not found in context");
|
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();
|
||||||
// Non-reactive counter so we can detect when staged_moves grows without
|
|
||||||
// returning a value from the Effect (which causes a Leptos reactive loop
|
|
||||||
// when the Effect also writes to the same signal it reads).
|
|
||||||
let prev_staged_len = Cell::new(0usize);
|
let prev_staged_len = Cell::new(0usize);
|
||||||
|
|
||||||
// ── Free-play mode ─────────────────────────────────────────────────────────
|
// ── Free-play mode ─────────────────────────────────────────────────────────
|
||||||
// When enabled the board shows all own-checker fields as valid origins and
|
|
||||||
// invalid moves produce an explanatory error rather than being suppressed.
|
|
||||||
fn load_free_mode() -> bool {
|
fn load_free_mode() -> bool {
|
||||||
gloo_storage::LocalStorage::get::<bool>("trictrac_free_mode").unwrap_or(false)
|
gloo_storage::LocalStorage::get::<bool>("trictrac_free_mode").unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
@ -62,13 +59,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
gloo_storage::LocalStorage::set("trictrac_free_mode", val).ok();
|
gloo_storage::LocalStorage::set("trictrac_free_mode", val).ok();
|
||||||
}
|
}
|
||||||
let free_mode: RwSignal<bool> = RwSignal::new(load_free_mode());
|
let free_mode: RwSignal<bool> = RwSignal::new(load_free_mode());
|
||||||
// None = no error; Some(None) = generic invalid; Some(Some(e)) = specific rule error
|
|
||||||
let move_error: RwSignal<Option<Option<MoveError>>> = RwSignal::new(None);
|
let move_error: RwSignal<Option<Option<MoveError>>> = RwSignal::new(None);
|
||||||
|
|
||||||
Effect::new(move |_| {
|
Effect::new(move |_| {
|
||||||
let moves = staged_moves.get();
|
let moves = staged_moves.get();
|
||||||
let n = moves.len();
|
let n = moves.len();
|
||||||
// Play checker sound whenever a move is added (own moves, immediate feedback).
|
|
||||||
if n > prev_staged_len.get() {
|
if n > prev_staged_len.get() {
|
||||||
crate::game::sound::play_checker_move();
|
crate::game::sound::play_checker_move();
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +76,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let m2 = to_cm(&moves[1]);
|
let m2 = to_cm(&moves[1]);
|
||||||
|
|
||||||
if free_mode.get_untracked() {
|
if free_mode.get_untracked() {
|
||||||
// Mirror moves to White-perspective for validation (MoveRules always works as White)
|
|
||||||
let (vm1, vm2) = if player_id == 0 {
|
let (vm1, vm2) = if player_id == 0 {
|
||||||
(m1, m2)
|
(m1, m2)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -90,14 +84,17 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let mut store_board = StoreBoard::new();
|
let mut store_board = StoreBoard::new();
|
||||||
store_board.set_positions(&Color::White, vs_board);
|
store_board.set_positions(&Color::White, vs_board);
|
||||||
let store_dice = StoreDice { values: vs_dice };
|
let store_dice = StoreDice { values: vs_dice };
|
||||||
let color = if player_id == 0 { Color::White } else { Color::Black };
|
let color = if player_id == 0 {
|
||||||
|
Color::White
|
||||||
|
} else {
|
||||||
|
Color::Black
|
||||||
|
};
|
||||||
let rules = MoveRules::new(&color, &store_board, store_dice);
|
let rules = MoveRules::new(&color, &store_board, store_dice);
|
||||||
if rules.moves_follow_rules(&(vm1, vm2)) {
|
if rules.moves_follow_rules(&(vm1, vm2)) {
|
||||||
cmd_tx_effect
|
cmd_tx_effect
|
||||||
.unbounded_send(NetCommand::Action(PlayerAction::Move(m1, m2)))
|
.unbounded_send(NetCommand::Action(PlayerAction::Move(m1, m2)))
|
||||||
.ok();
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
// moves_allowed gives the specific TricTrac rule that was broken (if any)
|
|
||||||
let specific_err = rules.moves_allowed(&(vm1, vm2)).err();
|
let specific_err = rules.moves_allowed(&(vm1, vm2)).err();
|
||||||
move_error.set(Some(specific_err));
|
move_error.set(Some(specific_err));
|
||||||
}
|
}
|
||||||
|
|
@ -109,20 +106,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
|
|
||||||
staged_moves.set(vec![]);
|
staged_moves.set(vec![]);
|
||||||
selected_origin.set(None);
|
selected_origin.set(None);
|
||||||
// Reset the counter so the next turn starts clean.
|
|
||||||
prev_staged_len.set(0);
|
prev_staged_len.set(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Auto-roll effect ─────────────────────────────────────────────────────
|
// ── 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.
|
|
||||||
// Guard: never auto-roll during the pre-game ceremony (the ceremony overlay
|
|
||||||
// has its own Roll button for PlayerAction::PreGameRoll).
|
|
||||||
let show_roll =
|
let show_roll =
|
||||||
is_my_turn && vs.turn_stage == SerTurnStage::RollDice && vs.stage != SerStage::PreGameRoll;
|
is_my_turn && vs.turn_stage == SerTurnStage::RollDice && vs.stage != SerStage::PreGameRoll;
|
||||||
if show_roll && !waiting_for_confirm {
|
if show_roll && !waiting_for_confirm {
|
||||||
|
|
@ -141,14 +129,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let cmd_tx_go = cmd_tx.clone();
|
let cmd_tx_go = 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();
|
||||||
// Only show the fallback Go button when there is no ScoringPanel showing it.
|
|
||||||
let show_hold_go = is_my_turn
|
let show_hold_go = is_my_turn
|
||||||
&& vs.turn_stage == SerTurnStage::HoldOrGoChoice
|
&& vs.turn_stage == SerTurnStage::HoldOrGoChoice
|
||||||
&& state.my_scored_event.is_none();
|
&& state.my_scored_event.is_none();
|
||||||
|
|
||||||
// ── Valid move sequences for this turn ─────────────────────────────────────
|
// ── Valid move sequences for this turn ─────────────────────────────────────
|
||||||
// Computed once per ViewState snapshot; used by Board (highlighting) and the
|
|
||||||
// empty-move button (visibility).
|
|
||||||
let valid_sequences: Vec<(CheckerMove, CheckerMove)> = if is_move_stage && dice != (0, 0) {
|
let valid_sequences: Vec<(CheckerMove, CheckerMove)> = if is_move_stage && dice != (0, 0) {
|
||||||
let mut store_board = StoreBoard::new();
|
let mut store_board = StoreBoard::new();
|
||||||
store_board.set_positions(&Color::White, vs.board);
|
store_board.set_positions(&Color::White, vs.board);
|
||||||
|
|
@ -170,14 +155,13 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
// Clone for the empty-move button reactive closure (Board consumes the original).
|
|
||||||
let valid_seqs_empty = valid_sequences.clone();
|
let valid_seqs_empty = valid_sequences.clone();
|
||||||
|
|
||||||
// ── Scores ─────────────────────────────────────────────────────────────────
|
// ── Scores ─────────────────────────────────────────────────────────────────
|
||||||
let my_score = vs.scores[player_id as usize].clone();
|
let my_score = vs.scores[player_id as usize].clone();
|
||||||
let opp_score = vs.scores[1 - player_id as usize].clone();
|
let opp_score = vs.scores[1 - player_id as usize].clone();
|
||||||
|
|
||||||
// ── Ceremony state (extracted before vs is moved into Board) ────────────────
|
// ── Ceremony state ──────────────────────────────────────────────────────────
|
||||||
let is_ceremony = vs.stage == SerStage::PreGameRoll;
|
let is_ceremony = vs.stage == SerStage::PreGameRoll;
|
||||||
let pre_game_roll_data: Option<PreGameRollState> = vs.pre_game_roll.clone();
|
let pre_game_roll_data: Option<PreGameRollState> = vs.pre_game_roll.clone();
|
||||||
let my_name_ceremony = my_score.name.clone();
|
let my_name_ceremony = my_score.name.clone();
|
||||||
|
|
@ -188,8 +172,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let my_scored_event = state.my_scored_event.clone();
|
let my_scored_event = state.my_scored_event.clone();
|
||||||
let opp_scored_event = state.opp_scored_event.clone();
|
let opp_scored_event = state.opp_scored_event.clone();
|
||||||
|
|
||||||
// Values for MergedScorePanel — extracted before events are consumed.
|
|
||||||
// Don't animate points when a hole was gained (points wrap around 12).
|
|
||||||
let my_pts_earned: u8 = my_scored_event.as_ref().map_or(0, |e| {
|
let my_pts_earned: u8 = my_scored_event.as_ref().map_or(0, |e| {
|
||||||
if e.holes_gained == 0 {
|
if e.holes_gained == 0 {
|
||||||
e.points_earned
|
e.points_earned
|
||||||
|
|
@ -214,7 +196,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
|
|
||||||
let last_moves = state.last_moves;
|
let last_moves = state.last_moves;
|
||||||
|
|
||||||
// fields where a battue (hit) was scored; ripple animation shown there.
|
|
||||||
let hit_fields: Vec<u8> = {
|
let hit_fields: Vec<u8> = {
|
||||||
let is_hit_jan = |jan: &Jan| {
|
let is_hit_jan = |jan: &Jan| {
|
||||||
matches!(
|
matches!(
|
||||||
|
|
@ -246,10 +227,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
fields
|
fields
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Sound effects (fire once on mount = once per state snapshot) ──────────
|
// ── Sound effects ──────────────────────────────────────────────────────────
|
||||||
// Dice roll: dice are fresh for the currently active player (Move stage means
|
|
||||||
// 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!(
|
let active_is_move_stage = matches!(
|
||||||
vs.turn_stage,
|
vs.turn_stage,
|
||||||
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
||||||
|
|
@ -257,12 +235,9 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
if show_dice && last_moves.is_none() && active_is_move_stage && !suppress_dice_anim {
|
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.
|
|
||||||
if last_moves.is_some() {
|
if last_moves.is_some() {
|
||||||
crate::game::sound::play_checker_move();
|
crate::game::sound::play_checker_move();
|
||||||
}
|
}
|
||||||
// Scoring: hole fanfare plays immediately; per-point ticks are driven by
|
|
||||||
// MergedScorePanel's counter animation so play_points_scored is not called here.
|
|
||||||
if let Some(ref ev) = my_scored_event {
|
if let Some(ref ev) = my_scored_event {
|
||||||
if ev.holes_gained > 0 {
|
if ev.holes_gained > 0 {
|
||||||
crate::game::sound::play_hole_scored();
|
crate::game::sound::play_hole_scored();
|
||||||
|
|
@ -282,6 +257,13 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let room_id = state.room_id.clone();
|
let room_id = state.room_id.clone();
|
||||||
let is_bot_game = state.is_bot_game;
|
let is_bot_game = state.is_bot_game;
|
||||||
|
|
||||||
|
// ── Active player indicator ────────────────────────────────────────────────
|
||||||
|
let active_player_is_me: Option<bool> = if stage == SerStage::InGame {
|
||||||
|
Some(is_my_turn)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// ── Game-over info ─────────────────────────────────────────────────────────
|
// ── Game-over info ─────────────────────────────────────────────────────────
|
||||||
let stage_is_ended = stage == SerStage::Ended;
|
let stage_is_ended = stage == SerStage::Ended;
|
||||||
let winner_is_me = my_score.holes >= 12;
|
let winner_is_me = my_score.holes >= 12;
|
||||||
|
|
@ -303,8 +285,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
// ── Game container ────────────────────────────────────────────────────
|
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
|
|
||||||
// ── Share popover (while waiting for opponent) ───────────────────
|
// ── Share popover (while waiting for opponent) ───────────────────
|
||||||
{(!is_bot_game && stage == SerStage::PreGame).then(|| {
|
{(!is_bot_game && stage == SerStage::PreGame).then(|| {
|
||||||
let url_label = share_url.clone();
|
let url_label = share_url.clone();
|
||||||
|
|
@ -346,20 +328,197 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|
||||||
// ── Merged scoreboard + scoring panels ─────────────
|
// ── Player strip (full-width, in-flow) ───────────────────────────
|
||||||
// score-area is position:relative so the scoring-panels-container
|
<MergedScorePanel
|
||||||
// can be absolute-positioned at the right of the hole counter.
|
my_score=my_score
|
||||||
<div class="score-area">
|
opp_score=opp_score
|
||||||
<MergedScorePanel
|
my_points_earned=my_pts_earned
|
||||||
my_score=my_score
|
opp_points_earned=opp_pts_earned
|
||||||
opp_score=opp_score
|
my_holes_gained=my_holes_gained_score
|
||||||
my_points_earned=my_pts_earned
|
opp_holes_gained=opp_holes_gained_score
|
||||||
opp_points_earned=opp_pts_earned
|
my_bredouille=my_bredouille_flash
|
||||||
my_holes_gained=my_holes_gained_score
|
active_player_is_me=active_player_is_me
|
||||||
opp_holes_gained=opp_holes_gained_score
|
/>
|
||||||
my_bredouille=my_bredouille_flash
|
|
||||||
|
// ── Board + controls (sidebar on wide, footer on narrow) ─────────
|
||||||
|
<div class="main-body">
|
||||||
|
<Board
|
||||||
|
view_state=vs
|
||||||
|
player_id=player_id
|
||||||
|
selected_origin=selected_origin
|
||||||
|
staged_moves=staged_moves
|
||||||
|
valid_sequences=valid_sequences
|
||||||
|
bar_dice=show_dice.then_some(dice)
|
||||||
|
bar_is_move=is_move_stage
|
||||||
|
is_my_turn=is_my_turn
|
||||||
|
bar_is_double=is_double_dice
|
||||||
|
last_moves=last_moves
|
||||||
|
hit_fields=hit_fields
|
||||||
|
suppress_dice_anim=suppress_dice_anim
|
||||||
|
free_mode=free_mode
|
||||||
/>
|
/>
|
||||||
// Scoring detail panels — stacked at the right, overlapping if needed.
|
|
||||||
|
// ── Controls: dice card + status/actions card ────────────────
|
||||||
|
<div class="controls">
|
||||||
|
{show_dice.then(|| view! {
|
||||||
|
<div class="ctrl-dice">
|
||||||
|
<div class="ctrl-dice-row">
|
||||||
|
{move || {
|
||||||
|
let staged = staged_moves.get();
|
||||||
|
let (u0, u1) = if suppress_dice_anim {
|
||||||
|
(true, true)
|
||||||
|
} else if is_move_stage {
|
||||||
|
bar_matched_dice_used(&staged, dice)
|
||||||
|
} else {
|
||||||
|
(false, false)
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<Die value=dice.0 used=u0 is_double=is_double_dice />
|
||||||
|
<Die value=dice.1 used=u1 is_double=is_double_dice />
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<label class="free-mode-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
prop:checked=move || free_mode.get()
|
||||||
|
on:change=move |ev| {
|
||||||
|
let v = event_target_checked(&ev);
|
||||||
|
save_free_mode(v);
|
||||||
|
free_mode.set(v);
|
||||||
|
move_error.set(None);
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{t!(i18n, free_mode_label)}
|
||||||
|
<span class="free-mode-help"
|
||||||
|
title=move || t_string!(i18n, free_mode_tooltip).to_owned()>
|
||||||
|
"?"
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div class="ctrl-status">
|
||||||
|
<div class="game-status">
|
||||||
|
{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),
|
||||||
|
PauseReason::AfterOpponentPreGameRoll => t_string!(i18n, after_opponent_pre_game_roll),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let n = staged_moves.get().len();
|
||||||
|
if is_move_stage {
|
||||||
|
t_string!(i18n, select_move, n = n + 1)
|
||||||
|
} else {
|
||||||
|
String::from(match (&stage, is_my_turn, &turn_stage) {
|
||||||
|
(SerStage::Ended, _, _) => t_string!(i18n, game_over),
|
||||||
|
(SerStage::PreGame, _, _) | (SerStage::PreGameRoll, _, _) => t_string!(i18n, waiting_for_opponent),
|
||||||
|
(SerStage::InGame, true, SerTurnStage::RollDice) => t_string!(i18n, your_turn_roll),
|
||||||
|
(SerStage::InGame, true, SerTurnStage::HoldOrGoChoice) => t_string!(i18n, hold_or_go),
|
||||||
|
(SerStage::InGame, true, _) => t_string!(i18n, your_turn),
|
||||||
|
(SerStage::InGame, false, _) => t_string!(i18n, opponent_turn),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{move || {
|
||||||
|
let hint: String = if waiting_for_confirm {
|
||||||
|
t_string!(i18n, hint_continue).to_owned()
|
||||||
|
} else if is_move_stage {
|
||||||
|
t_string!(i18n, hint_move).to_owned()
|
||||||
|
} else if is_my_turn && turn_stage_for_sub == SerTurnStage::HoldOrGoChoice {
|
||||||
|
t_string!(i18n, hint_hold_or_go).to_owned()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
(!hint.is_empty()).then(|| view! { <p class="game-sub-prompt">{hint}</p> })
|
||||||
|
}}
|
||||||
|
// ── Free-mode error banner ─────────────────────────────
|
||||||
|
{move || {
|
||||||
|
move_error.get().map(|opt_err| {
|
||||||
|
let msg: String = match opt_err {
|
||||||
|
None => t_string!(i18n, err_invalid_move).to_owned(),
|
||||||
|
Some(MoveError::OpponentCorner) => t_string!(i18n, err_opponent_corner).to_owned(),
|
||||||
|
Some(MoveError::CornerNeedsTwoCheckers) => t_string!(i18n, err_corner_needs_two).to_owned(),
|
||||||
|
Some(MoveError::CornerByEffectPossible) => t_string!(i18n, err_corner_by_effect).to_owned(),
|
||||||
|
Some(MoveError::ExitNeedsAllCheckersOnLastQuarter) => t_string!(i18n, err_exit_needs_all_in_last_jan).to_owned(),
|
||||||
|
Some(MoveError::ExitByEffectPossible) => t_string!(i18n, err_exit_by_effect).to_owned(),
|
||||||
|
Some(MoveError::ExitNotFarthest) => t_string!(i18n, err_exit_not_farthest).to_owned(),
|
||||||
|
Some(MoveError::OpponentCanFillQuarter) => t_string!(i18n, err_opponent_can_fill_quarter).to_owned(),
|
||||||
|
Some(MoveError::MustFillQuarter) => t_string!(i18n, err_must_fill_quarter).to_owned(),
|
||||||
|
Some(MoveError::MustPlayAllDice) => t_string!(i18n, err_must_play_all_dice).to_owned(),
|
||||||
|
Some(MoveError::MustPlayStrongerDie) => t_string!(i18n, err_must_play_stronger_die).to_owned(),
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<div class="free-mode-error">
|
||||||
|
<span class="free-mode-error-msg">{msg}</span>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
on:click=move |_| { move_error.set(None); }
|
||||||
|
>{t!(i18n, reset_move)}</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<div class="board-actions">
|
||||||
|
{waiting_for_confirm.then(|| view! {
|
||||||
|
<button class="btn btn-primary" on:click=move |_| {
|
||||||
|
pending.update(|q| { q.pop_front(); });
|
||||||
|
}>{t!(i18n, continue_btn)}</button>
|
||||||
|
})}
|
||||||
|
{show_hold_go.then(|| view! {
|
||||||
|
<button class="btn btn-primary" on:click=move |_| {
|
||||||
|
cmd_tx_go.unbounded_send(NetCommand::Action(PlayerAction::Go)).ok();
|
||||||
|
}>{t!(i18n, go)}</button>
|
||||||
|
})}
|
||||||
|
{move || {
|
||||||
|
let staged = staged_moves.get();
|
||||||
|
let show = is_move_stage && staged.len() < 2 && (
|
||||||
|
valid_seqs_empty.is_empty() || match staged.len() {
|
||||||
|
0 => valid_seqs_empty.iter().any(|(m1, _)| m1.get_from() == 0),
|
||||||
|
1 => {
|
||||||
|
let (f0, t0) = staged[0];
|
||||||
|
valid_seqs_empty.iter()
|
||||||
|
.filter(|(m1, _)| {
|
||||||
|
m1.get_from() as u8 == f0
|
||||||
|
&& m1.get_to() as u8 == t0
|
||||||
|
})
|
||||||
|
.any(|(_, m2)| m2.get_from() == 0)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
show.then(|| view! {
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
on:click=move |_| {
|
||||||
|
selected_origin.set(None);
|
||||||
|
staged_moves.update(|v| v.push((0, 0)));
|
||||||
|
}
|
||||||
|
>{t!(i18n, empty_move)}</button>
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
{move || {
|
||||||
|
(is_move_stage && staged_moves.get().len() == 1).then(|| view! {
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
on:click=move |_| {
|
||||||
|
staged_moves.set(vec![]);
|
||||||
|
selected_origin.set(None);
|
||||||
|
}
|
||||||
|
>{t!(i18n, cancel_move)}</button>
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// ── Scoring notification panels ───────────────────────────────────
|
||||||
|
<div class="scoring-row">
|
||||||
<div class="scoring-panels-container">
|
<div class="scoring-panels-container">
|
||||||
{my_scored_event.map(|event| view! {
|
{my_scored_event.map(|event| view! {
|
||||||
<ScoringPanel event=event turn_stage=turn_stage_for_panel />
|
<ScoringPanel event=event turn_stage=turn_stage_for_panel />
|
||||||
|
|
@ -370,157 +529,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// ── Board ────────────────────────────────────────────────────────
|
|
||||||
<Board
|
|
||||||
view_state=vs
|
|
||||||
player_id=player_id
|
|
||||||
selected_origin=selected_origin
|
|
||||||
staged_moves=staged_moves
|
|
||||||
valid_sequences=valid_sequences
|
|
||||||
bar_dice=show_dice.then_some(dice)
|
|
||||||
bar_is_move=is_move_stage
|
|
||||||
is_my_turn=is_my_turn
|
|
||||||
bar_is_double=is_double_dice
|
|
||||||
last_moves=last_moves
|
|
||||||
hit_fields=hit_fields
|
|
||||||
suppress_dice_anim=suppress_dice_anim
|
|
||||||
free_mode=free_mode
|
|
||||||
/>
|
|
||||||
|
|
||||||
// ── Status, hints, and actions — cream strip below board ─
|
|
||||||
<div class="game-bottom-strip">
|
|
||||||
<div class="game-status">
|
|
||||||
{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),
|
|
||||||
PauseReason::AfterOpponentPreGameRoll => t_string!(i18n, after_opponent_pre_game_roll),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let n = staged_moves.get().len();
|
|
||||||
if is_move_stage {
|
|
||||||
t_string!(i18n, select_move, n = n + 1)
|
|
||||||
} else {
|
|
||||||
String::from(match (&stage, is_my_turn, &turn_stage) {
|
|
||||||
(SerStage::Ended, _, _) => t_string!(i18n, game_over),
|
|
||||||
(SerStage::PreGame, _, _) | (SerStage::PreGameRoll, _, _) => t_string!(i18n, waiting_for_opponent),
|
|
||||||
(SerStage::InGame, true, SerTurnStage::RollDice) => t_string!(i18n, your_turn_roll),
|
|
||||||
(SerStage::InGame, true, SerTurnStage::HoldOrGoChoice) => t_string!(i18n, hold_or_go),
|
|
||||||
(SerStage::InGame, true, _) => t_string!(i18n, your_turn),
|
|
||||||
(SerStage::InGame, false, _) => t_string!(i18n, opponent_turn),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
{move || {
|
|
||||||
let hint: String = if waiting_for_confirm {
|
|
||||||
t_string!(i18n, hint_continue).to_owned()
|
|
||||||
} else if is_move_stage {
|
|
||||||
t_string!(i18n, hint_move).to_owned()
|
|
||||||
} else if is_my_turn && turn_stage_for_sub == SerTurnStage::HoldOrGoChoice {
|
|
||||||
t_string!(i18n, hint_hold_or_go).to_owned()
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
(!hint.is_empty()).then(|| view! { <p class="game-sub-prompt">{hint}</p> })
|
|
||||||
}}
|
|
||||||
// ── Free-mode error banner ─────────────────────────────────────
|
|
||||||
{move || {
|
|
||||||
move_error.get().map(|opt_err| {
|
|
||||||
let msg: String = match opt_err {
|
|
||||||
None => t_string!(i18n, err_invalid_move).to_owned(),
|
|
||||||
Some(MoveError::OpponentCorner) => t_string!(i18n, err_opponent_corner).to_owned(),
|
|
||||||
Some(MoveError::CornerNeedsTwoCheckers) => t_string!(i18n, err_corner_needs_two).to_owned(),
|
|
||||||
Some(MoveError::CornerByEffectPossible) => t_string!(i18n, err_corner_by_effect).to_owned(),
|
|
||||||
Some(MoveError::ExitNeedsAllCheckersOnLastQuarter) => t_string!(i18n, err_exit_needs_all_in_last_jan).to_owned(),
|
|
||||||
Some(MoveError::ExitByEffectPossible) => t_string!(i18n, err_exit_by_effect).to_owned(),
|
|
||||||
Some(MoveError::ExitNotFarthest) => t_string!(i18n, err_exit_not_farthest).to_owned(),
|
|
||||||
Some(MoveError::OpponentCanFillQuarter) => t_string!(i18n, err_opponent_can_fill_quarter).to_owned(),
|
|
||||||
Some(MoveError::MustFillQuarter) => t_string!(i18n, err_must_fill_quarter).to_owned(),
|
|
||||||
Some(MoveError::MustPlayAllDice) => t_string!(i18n, err_must_play_all_dice).to_owned(),
|
|
||||||
Some(MoveError::MustPlayStrongerDie) => t_string!(i18n, err_must_play_stronger_die).to_owned(),
|
|
||||||
};
|
|
||||||
view! {
|
|
||||||
<div class="free-mode-error">
|
|
||||||
<span class="free-mode-error-msg">{msg}</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary"
|
|
||||||
on:click=move |_| { move_error.set(None); }
|
|
||||||
>{t!(i18n, reset_move)}</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
<div class="board-actions">
|
|
||||||
{waiting_for_confirm.then(|| view! {
|
|
||||||
<button class="btn btn-primary" on:click=move |_| {
|
|
||||||
pending.update(|q| { q.pop_front(); });
|
|
||||||
}>{t!(i18n, continue_btn)}</button>
|
|
||||||
})}
|
|
||||||
// Fallback Go button when no scoring panel (e.g. after reconnect)
|
|
||||||
{show_hold_go.then(|| view! {
|
|
||||||
<button class="btn btn-primary" on:click=move |_| {
|
|
||||||
cmd_tx_go.unbounded_send(NetCommand::Action(PlayerAction::Go)).ok();
|
|
||||||
}>{t!(i18n, go)}</button>
|
|
||||||
})}
|
|
||||||
{move || {
|
|
||||||
let staged = staged_moves.get();
|
|
||||||
let show = is_move_stage && staged.len() < 2 && (
|
|
||||||
valid_seqs_empty.is_empty() || match staged.len() {
|
|
||||||
0 => valid_seqs_empty.iter().any(|(m1, _)| m1.get_from() == 0),
|
|
||||||
1 => {
|
|
||||||
let (f0, t0) = staged[0];
|
|
||||||
valid_seqs_empty.iter()
|
|
||||||
.filter(|(m1, _)| {
|
|
||||||
m1.get_from() as u8 == f0
|
|
||||||
&& m1.get_to() as u8 == t0
|
|
||||||
})
|
|
||||||
.any(|(_, m2)| m2.get_from() == 0)
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
show.then(|| view! {
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary"
|
|
||||||
on:click=move |_| {
|
|
||||||
selected_origin.set(None);
|
|
||||||
staged_moves.update(|v| v.push((0, 0)));
|
|
||||||
}
|
|
||||||
>{t!(i18n, empty_move)}</button>
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
{move || {
|
|
||||||
(is_move_stage && staged_moves.get().len() == 1).then(|| view! {
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary"
|
|
||||||
on:click=move |_| {
|
|
||||||
staged_moves.set(vec![]);
|
|
||||||
selected_origin.set(None);
|
|
||||||
}
|
|
||||||
>{t!(i18n, cancel_move)}</button>
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
// ── Free-play mode toggle ─────────────────────────────────────
|
|
||||||
<label class="free-mode-toggle">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
prop:checked=move || free_mode.get()
|
|
||||||
on:change=move |ev| {
|
|
||||||
let v = event_target_checked(&ev);
|
|
||||||
save_free_mode(v);
|
|
||||||
free_mode.set(v);
|
|
||||||
move_error.set(None);
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{t!(i18n, free_mode_label)}
|
|
||||||
<span class="free-mode-help" title=move || t_string!(i18n, free_mode_tooltip).to_owned()>"?"</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// ── Pre-game ceremony overlay ─────────────────────────────────────
|
// ── Pre-game ceremony overlay ─────────────────────────────────────
|
||||||
{is_ceremony.then(|| {
|
{is_ceremony.then(|| {
|
||||||
let pgr = pre_game_roll_data.unwrap_or(PreGameRollState {
|
let pgr = pre_game_roll_data.unwrap_or(PreGameRollState {
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,18 @@ pub fn jan_label(jan: &Jan) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merged scoreboard showing both players above the board.
|
/// Full-width player strip at the top of the game screen.
|
||||||
///
|
///
|
||||||
/// - Two stacked rows for a clear race-to-12 visual comparison.
|
/// - Left side: me (right-aligned toward center): avatar → name → pegs → pts.
|
||||||
/// - Points shown as an animated jackpot counter (ticks up on each new point).
|
/// - Center: "Trictrac" italic title.
|
||||||
/// - Hole pegs are larger and use green (me) / red (opponent) instead of gold.
|
/// - Right side: opponent (left-aligned from center): pts → pegs → name → avatar.
|
||||||
/// - When a hole is gained, the new peg pops in and a brief non-blocking label
|
/// - Active player zone gets a subtle rounded highlight.
|
||||||
/// appears instead of the old blocking toast popup.
|
/// - Points animate as a jackpot counter; new peg pops in with an animation.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MergedScorePanel(
|
pub fn MergedScorePanel(
|
||||||
my_score: PlayerScore,
|
my_score: PlayerScore,
|
||||||
opp_score: PlayerScore,
|
opp_score: PlayerScore,
|
||||||
/// Points just earned this turn; 0 = no animation. Set to 0 when a hole
|
/// Points just earned this turn; 0 = no animation.
|
||||||
/// was gained (points wrap around 12, counter stays at end value).
|
|
||||||
#[prop(default = 0)]
|
#[prop(default = 0)]
|
||||||
my_points_earned: u8,
|
my_points_earned: u8,
|
||||||
#[prop(default = 0)] opp_points_earned: u8,
|
#[prop(default = 0)] opp_points_earned: u8,
|
||||||
|
|
@ -55,14 +54,13 @@ pub fn MergedScorePanel(
|
||||||
/// True when my hole was scored under bredouille (shows ×2 in the flash).
|
/// True when my hole was scored under bredouille (shows ×2 in the flash).
|
||||||
#[prop(default = false)]
|
#[prop(default = false)]
|
||||||
my_bredouille: bool,
|
my_bredouille: bool,
|
||||||
|
/// `Some(true)` = my turn active, `Some(false)` = opponent active, `None` = no active turn.
|
||||||
|
#[prop(default = None)]
|
||||||
|
active_player_is_me: Option<bool>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
|
|
||||||
// ── Points counter signals ──────────────────────────────────────────────
|
// ── Points counter signals ──────────────────────────────────────────────
|
||||||
// When no hole was gained: start from (current - earned) and tick up.
|
|
||||||
// When a hole was gained: points wrapped around 12, so skip the animation.
|
|
||||||
// On non-WASM there is no animation; start directly at the final value.
|
|
||||||
// Suppress the unused-variable warning for animation-only params.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = (my_points_earned, opp_points_earned);
|
let _ = (my_points_earned, opp_points_earned);
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -122,10 +120,6 @@ pub fn MergedScorePanel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Ghost bar widths (show the end value immediately — static reference) ─
|
|
||||||
let my_bar_style = format!("width:{}%", (my_score.points as u32 * 100 / 12).min(100));
|
|
||||||
let opp_bar_style = format!("width:{}%", (opp_score.points as u32 * 100 / 12).min(100));
|
|
||||||
|
|
||||||
// ── Hole peg tracks ─────────────────────────────────────────────────────
|
// ── Hole peg tracks ─────────────────────────────────────────────────────
|
||||||
let my_holes = my_score.holes;
|
let my_holes = my_score.holes;
|
||||||
let opp_holes = opp_score.holes;
|
let opp_holes = opp_score.holes;
|
||||||
|
|
@ -163,73 +157,77 @@ pub fn MergedScorePanel(
|
||||||
let my_can_bredouille = my_score.can_bredouille;
|
let my_can_bredouille = my_score.can_bredouille;
|
||||||
let opp_can_bredouille = opp_score.can_bredouille;
|
let opp_can_bredouille = opp_score.can_bredouille;
|
||||||
|
|
||||||
|
let my_active = active_player_is_me == Some(true);
|
||||||
|
let opp_active = active_player_is_me == Some(false);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="merged-score-panel">
|
<div class="players-strip">
|
||||||
|
|
||||||
// ── My player row ───────────────────────────────────────────
|
// ── My player: left side, right-aligned toward center ───────────
|
||||||
<div class="score-row score-row-me">
|
<div class="strip-player strip-player-left">
|
||||||
<div class="score-row-name">
|
<div class="strip-active-zone" class:active=my_active>
|
||||||
<span class="player-name">{my_name}</span>
|
<div class="strip-avatar strip-avatar-me"></div>
|
||||||
<span class="you-tag">{t!(i18n, you_suffix)}</span>
|
<div class="score-row-name">
|
||||||
</div>
|
<span class="player-name">{my_name}</span>
|
||||||
<div class="pts-counter-wrap">
|
<span class="you-tag">{t!(i18n, you_suffix)}</span>
|
||||||
<div class="pts-ghost-bar-track">
|
|
||||||
<div class="pts-ghost-bar-fill" style=my_bar_style></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pts-counter-row">
|
{my_can_bredouille.then(|| view! {
|
||||||
<span class="pts-counter">{move || my_displayed_pts.get()}</span>
|
<span class="bredouille-badge"
|
||||||
<span class="pts-max">"/12"</span>
|
title=move || t_string!(i18n, bredouille_title).to_owned()>
|
||||||
</div>
|
"B"
|
||||||
</div>
|
</span>
|
||||||
<div class="peg-track">{my_pegs}</div>
|
})}
|
||||||
{my_can_bredouille.then(|| view! {
|
<div class="peg-track">{my_pegs}</div>
|
||||||
<span class="bredouille-badge"
|
<div class="pts-counter-wrap">
|
||||||
title=move || t_string!(i18n, bredouille_title).to_owned()>
|
<div class="pts-counter-row">
|
||||||
"B"
|
<span class="pts-counter">{move || my_displayed_pts.get()}</span>
|
||||||
</span>
|
<span class="pts-max">"/12"</span>
|
||||||
})}
|
|
||||||
// Flash sits in the free space to the right of the pegs.
|
|
||||||
// margin-left:auto keeps it right-aligned inside the flex row
|
|
||||||
// without adding a new row, so the board never shifts down.
|
|
||||||
{(my_holes_gained > 0).then(|| {
|
|
||||||
let label = if my_bredouille {
|
|
||||||
format!("Trou {} · ×2 bredouille", my_holes)
|
|
||||||
} else {
|
|
||||||
format!("Trou {}", my_holes)
|
|
||||||
};
|
|
||||||
view! {
|
|
||||||
<div class="hole-flash"
|
|
||||||
class:hole-flash-bredouille=my_bredouille>
|
|
||||||
{label}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
})}
|
{(my_holes_gained > 0).then(|| {
|
||||||
|
let label = if my_bredouille {
|
||||||
|
format!("Trou {} · ×2 bredouille", my_holes)
|
||||||
|
} else {
|
||||||
|
format!("Trou {}", my_holes)
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<div class="hole-flash"
|
||||||
|
class:hole-flash-bredouille=my_bredouille>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="score-row-sep"></div>
|
// ── Center title ────────────────────────────────────────────────
|
||||||
|
<div class="strip-center">
|
||||||
// ── Opponent row ────────────────────────────────────────────
|
<span class="strip-title">"Trictrac"</span>
|
||||||
<div class="score-row score-row-opp">
|
|
||||||
<div class="score-row-name">
|
|
||||||
<span class="player-name">{opp_name}</span>
|
|
||||||
</div>
|
|
||||||
<div class="pts-counter-wrap">
|
|
||||||
<div class="pts-ghost-bar-track">
|
|
||||||
<div class="pts-ghost-bar-fill pts-ghost-bar-opp" style=opp_bar_style></div>
|
|
||||||
</div>
|
|
||||||
<div class="pts-counter-row">
|
|
||||||
<span class="pts-counter">{move || opp_displayed_pts.get()}</span>
|
|
||||||
<span class="pts-max">"/12"</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="peg-track">{opp_pegs}</div>
|
|
||||||
{opp_can_bredouille.then(|| view! {
|
|
||||||
<span class="bredouille-badge"
|
|
||||||
title=move || t_string!(i18n, bredouille_title).to_owned()>
|
|
||||||
"B"
|
|
||||||
</span>
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
// ── Opponent: right side, left-aligned from center ──────────────
|
||||||
|
<div class="strip-player strip-player-right">
|
||||||
|
<div class="strip-active-zone" class:active=opp_active>
|
||||||
|
<div class="pts-counter-wrap">
|
||||||
|
<div class="pts-counter-row">
|
||||||
|
<span class="pts-counter">{move || opp_displayed_pts.get()}</span>
|
||||||
|
<span class="pts-max">"/12"</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="peg-track">{opp_pegs}</div>
|
||||||
|
{opp_can_bredouille.then(|| view! {
|
||||||
|
<span class="bredouille-badge"
|
||||||
|
title=move || t_string!(i18n, bredouille_title).to_owned()>
|
||||||
|
"B"
|
||||||
|
</span>
|
||||||
|
})}
|
||||||
|
<div class="score-row-name">
|
||||||
|
<span class="player-name">{opp_name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="strip-avatar strip-avatar-opp"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
64
devenv.lock
64
devenv.lock
|
|
@ -17,62 +17,6 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-compat": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1767039857,
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"git-hooks": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"gitignore": "gitignore",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1778507602,
|
|
||||||
"owner": "cachix",
|
|
||||||
"repo": "git-hooks.nix",
|
|
||||||
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "cachix",
|
|
||||||
"repo": "git-hooks.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gitignore": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"git-hooks",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1762808025,
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "gitignore.nix",
|
|
||||||
"rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "gitignore.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1779102034,
|
"lastModified": 1779102034,
|
||||||
|
|
@ -108,15 +52,11 @@
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"devenv": "devenv",
|
"devenv": "devenv",
|
||||||
"git-hooks": "git-hooks",
|
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"nixpkgs-cmake3": "nixpkgs-cmake3",
|
"nixpkgs-cmake3": "nixpkgs-cmake3"
|
||||||
"pre-commit-hooks": [
|
|
||||||
"git-hooks"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
"version": 7
|
"version": 7
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
711
doc/design/variations/07-scrolling-header.html
Normal file
711
doc/design/variations/07-scrolling-header.html
Normal file
|
|
@ -0,0 +1,711 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Variation 07 — Scrolling header · Responsive sidebar/footer</title>
|
||||||
|
<link rel="stylesheet" href="../snapshots/2026-05-30-d4a2ea1c531827bfbc2410af20a00e349d606c87_files/style.css">
|
||||||
|
<style>
|
||||||
|
/* ══════════════════════════════════════════════════════════════════
|
||||||
|
Variation 07 — from 06, with:
|
||||||
|
1. Strip scrolls with the page (in-flow, not fixed).
|
||||||
|
Breaks out of game-overlay padding via 100vw + margin trick.
|
||||||
|
2. Center: "Trictrac" title only (no VS / game-info / turn).
|
||||||
|
3. No progress bar under the score counter.
|
||||||
|
4. Circles are 38 px, styled as real white/black checkers.
|
||||||
|
5. Active player gets a slightly darker rounded background
|
||||||
|
spanning from circle to score counter.
|
||||||
|
6. Controls area styled like v02: separate rounded cards,
|
||||||
|
0.5rem gap, game-status color: var(--ui-ink).
|
||||||
|
7. Responsive: ≥920px → dice+status in right sidebar column;
|
||||||
|
<920px → dice+status in a bottom footer bar.
|
||||||
|
══════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── Suppress baseline elements ──────────────────────────────────── */
|
||||||
|
.score-area { display: none !important; }
|
||||||
|
.board-bar { width: 5px; overflow: hidden; }
|
||||||
|
.bar-die-slot { display: none; }
|
||||||
|
.zone-labels-row { display: none; }
|
||||||
|
.game-bottom-strip { display: none; }
|
||||||
|
|
||||||
|
/* Allow strip to extend beyond padded overlay without scrollbar */
|
||||||
|
.game-overlay { overflow-x: hidden !important; }
|
||||||
|
.game-container { gap: 0.5rem; }
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════════════
|
||||||
|
PLAYER STRIP — in-flow, full viewport width, touches top edge.
|
||||||
|
|
||||||
|
The overlay has position:fixed; inset:0; padding:1.5rem so the
|
||||||
|
game-container starts 1.5rem below the viewport top.
|
||||||
|
|
||||||
|
margin-top: -1.5rem → pulls strip flush to viewport top
|
||||||
|
width: 100vw → fills the full viewport width
|
||||||
|
margin-left: calc(50% - 50vw)
|
||||||
|
50% = half of the game-container's content width (centered)
|
||||||
|
50vw = half of the viewport width
|
||||||
|
→ net offset puts the left edge at the viewport left edge
|
||||||
|
════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.v07-strip {
|
||||||
|
width: 100vw;
|
||||||
|
margin-top: -1.5rem;
|
||||||
|
margin-left: calc(50% - 50vw);
|
||||||
|
/* stays in normal flex flow — scrolls with the overlay */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--ui-parchment);
|
||||||
|
border-bottom: 2px solid var(--ui-gold-dark);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||||
|
padding: 0.35rem 1.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Player halves ── */
|
||||||
|
.v07-player {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
/* Left (me): push active-zone rightward toward VS */
|
||||||
|
.v07-player-left { justify-content: flex-end; }
|
||||||
|
/* Right (opp): push active-zone leftward toward VS */
|
||||||
|
.v07-player-right { justify-content: flex-start; }
|
||||||
|
|
||||||
|
/* ── Active zone: inner container that receives the highlight ── */
|
||||||
|
.v07-active-zone {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.28rem 0.5rem;
|
||||||
|
}
|
||||||
|
.v07-active-zone.active {
|
||||||
|
background: rgba(58,42,10,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Checker-style avatar circles ── */
|
||||||
|
.v07-avatar {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
/* White checker gradient (me) */
|
||||||
|
.v07-avatar-me {
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse 50% 35% at 36% 30%,
|
||||||
|
rgba(255,255,255,0.65) 0%, transparent 100%),
|
||||||
|
radial-gradient(circle,
|
||||||
|
transparent 68%, rgba(160,130,70,0.22) 68.5%,
|
||||||
|
rgba(160,130,70,0.22) 71.5%, transparent 72%),
|
||||||
|
radial-gradient(circle,
|
||||||
|
transparent 43%, rgba(160,130,70,0.17) 43.5%,
|
||||||
|
rgba(160,130,70,0.17) 46.5%, transparent 47%),
|
||||||
|
radial-gradient(circle at 38% 32%,
|
||||||
|
#ffffff 0%, #f5edd8 52%, #c0b288 100%);
|
||||||
|
border: 1.8px solid #c8a448;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.35), inset 0 -1px 3px rgba(0,0,0,0.15);
|
||||||
|
color: #443322;
|
||||||
|
}
|
||||||
|
/* Black checker gradient (opp) */
|
||||||
|
.v07-avatar-opp {
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse 40% 28% at 36% 30%,
|
||||||
|
rgba(110,65,30,0.38) 0%, transparent 100%),
|
||||||
|
radial-gradient(circle,
|
||||||
|
transparent 68%, rgba(200,164,72,0.18) 68.5%,
|
||||||
|
rgba(200,164,72,0.18) 71.5%, transparent 72%),
|
||||||
|
radial-gradient(circle,
|
||||||
|
transparent 43%, rgba(200,164,72,0.13) 43.5%,
|
||||||
|
rgba(200,164,72,0.13) 46.5%, transparent 47%),
|
||||||
|
radial-gradient(circle at 38% 32%,
|
||||||
|
#4a2e1a 0%, #1c1008 45%, #1a0f06 100%);
|
||||||
|
border: 1.8px solid #c8a448;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.5), inset 0 -1px 3px rgba(0,0,0,0.4);
|
||||||
|
color: #c8b898;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Peg holes inside the strip ── */
|
||||||
|
.v07-strip .peg-track { gap: 3px; }
|
||||||
|
.v07-strip .peg-hole { width: 12px; height: 12px; }
|
||||||
|
.v07-strip .peg-hole.filled {
|
||||||
|
background: #5aab38; border-color: #3a7828;
|
||||||
|
box-shadow: 0 0 5px rgba(90,171,56,0.55);
|
||||||
|
}
|
||||||
|
.v07-strip .peg-hole.peg-opp.filled {
|
||||||
|
background: #c05030; border-color: #8a3018;
|
||||||
|
box-shadow: 0 0 5px rgba(192,80,48,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Name block: auto width (not the 120px baseline) ── */
|
||||||
|
.v07-strip .score-row-name { width: auto; }
|
||||||
|
|
||||||
|
/* ── Score counter: no ghost bar ── */
|
||||||
|
.v07-strip .pts-counter-wrap { padding-bottom: 0; }
|
||||||
|
|
||||||
|
/* ── Center: "Trictrac" title only ── */
|
||||||
|
.v07-strip-center {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 1rem;
|
||||||
|
border-left: 1px solid rgba(138,106,40,0.2);
|
||||||
|
border-right: 1px solid rgba(138,106,40,0.2);
|
||||||
|
}
|
||||||
|
.v07-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--ui-ink);
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════════════
|
||||||
|
BODY — flex row: [board-wrapper] [sidebar]
|
||||||
|
════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.v07-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════════════
|
||||||
|
RIGHT SIDEBAR (wide ≥920px) — matches v02 .game-right-sidebar style
|
||||||
|
════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.v07-sidebar {
|
||||||
|
width: 152px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark-wood dice card */
|
||||||
|
.v07-sidebar-dice {
|
||||||
|
background: var(--board-rail);
|
||||||
|
border-radius: 5px;
|
||||||
|
border-top: 2px solid var(--ui-gold-dark);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||||
|
padding: 0.6rem 0.75rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.v07-sidebar-dice-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.55rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.v07-sidebar-dice .free-mode-toggle {
|
||||||
|
color: var(--ui-parchment);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
.v07-sidebar-dice .free-mode-help {
|
||||||
|
border-color: rgba(242,232,208,0.35);
|
||||||
|
color: rgba(242,232,208,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parchment status card */
|
||||||
|
.v07-sidebar-status {
|
||||||
|
background: var(--ui-parchment);
|
||||||
|
border-radius: 5px;
|
||||||
|
border-top: 2px solid var(--ui-gold-dark);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||||
|
padding: 0.65rem 0.75rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.v07-sidebar-status .game-status {
|
||||||
|
color: var(--ui-ink);
|
||||||
|
text-shadow: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.v07-sidebar-status .board-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════════════
|
||||||
|
BOTTOM BAR (narrow <920px) — v06 two-section style + v02 rounding
|
||||||
|
════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.v07-bottom-bar {
|
||||||
|
display: none; /* shown via media query */
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.5rem; /* v02: 0.5rem gap between sections */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark-wood dice section */
|
||||||
|
.v07-foot-dice {
|
||||||
|
background: var(--board-rail);
|
||||||
|
border-radius: 5px; /* v02 round corners */
|
||||||
|
border-top: 2px solid var(--ui-gold-dark);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||||
|
padding: 0.6rem 0.75rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.v07-foot-dice-row { display: flex; gap: 0.5rem; align-items: center; }
|
||||||
|
.v07-foot-dice .free-mode-toggle {
|
||||||
|
color: var(--ui-parchment);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
.v07-foot-dice .free-mode-help {
|
||||||
|
border-color: rgba(242,232,208,0.35);
|
||||||
|
color: rgba(242,232,208,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parchment status section */
|
||||||
|
.v07-foot-main {
|
||||||
|
background: var(--ui-parchment);
|
||||||
|
border-radius: 5px; /* v02 round corners */
|
||||||
|
border-top: 2px solid var(--ui-gold-dark);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||||
|
padding: 0.65rem 0.75rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.v07-foot-main .game-status {
|
||||||
|
color: var(--ui-ink); /* v02 explicit colour */
|
||||||
|
text-shadow: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.v07-foot-main .board-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════════════
|
||||||
|
SCORING ROW — below bottom-bar (or below sidebar+board on wide)
|
||||||
|
════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.v07-scoring-row { width: 100%; }
|
||||||
|
|
||||||
|
.v07-scoring-row .scoring-panels-container {
|
||||||
|
position: static;
|
||||||
|
top: auto; left: auto; z-index: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Green: local player scored */
|
||||||
|
.v07-scoring-row .scoring-panel {
|
||||||
|
background: #edf7ee;
|
||||||
|
border: 1px solid #a8d4b0;
|
||||||
|
border-left: 3px solid #2d7a3c;
|
||||||
|
box-shadow: 0 2px 8px rgba(42,107,60,0.10);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.v07-scoring-row .scoring-total { color: #1e5829; }
|
||||||
|
.v07-scoring-row .jan-label { color: #2a3d28; }
|
||||||
|
.v07-scoring-row .jan-tag { color: rgba(42,80,42,0.6); background: rgba(42,107,60,0.07); }
|
||||||
|
.v07-scoring-row .jan-pts { color: #1e5829; }
|
||||||
|
.v07-scoring-row .scoring-collapse-btn { color: rgba(42,80,42,0.4); }
|
||||||
|
.v07-scoring-row .scoring-expand-btn { background: #edf7ee; border-color: #a8d4b0; color: #2d7a3c; }
|
||||||
|
|
||||||
|
/* Red: opponent scored */
|
||||||
|
.v07-scoring-row .scoring-panel.opp-scored {
|
||||||
|
background: #fceaea;
|
||||||
|
border-color: #dea8a8;
|
||||||
|
border-left-color: #b52b2b;
|
||||||
|
}
|
||||||
|
.v07-scoring-row .scoring-panel.opp-scored .scoring-total { color: #7a1e1e; }
|
||||||
|
.v07-scoring-row .scoring-panel.opp-scored .jan-pts { color: #7a1e1e; }
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════════════
|
||||||
|
RESPONSIVE — 920px breakpoint
|
||||||
|
════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* Wide (≥920px): show sidebar, hide bottom bar */
|
||||||
|
@media (min-width: 920px) {
|
||||||
|
.v07-sidebar { display: flex; }
|
||||||
|
.v07-bottom-bar { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Narrow (<920px): hide sidebar, show bottom bar */
|
||||||
|
@media (max-width: 919px) {
|
||||||
|
.v07-sidebar { display: none !important; }
|
||||||
|
.v07-bottom-bar { display: flex; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Very narrow: hide peg tracks in header */
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.v07-strip .peg-track { display: none; }
|
||||||
|
.v07-strip-center { padding: 0 0.5rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ── Left navigation sidebar ───────────────────────────────────────── -->
|
||||||
|
<button class="game-hamburger game-hamburger-open" aria-label="Menu">
|
||||||
|
<span class="hb-bar hb-top"></span>
|
||||||
|
<span class="hb-bar hb-mid"></span>
|
||||||
|
<span class="hb-bar hb-bot"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="game-sidebar game-sidebar-open">
|
||||||
|
<div class="game-sidebar-header">
|
||||||
|
<span class="game-sidebar-brand">Trictrac</span>
|
||||||
|
<div class="lang-switcher">
|
||||||
|
<button>EN</button>
|
||||||
|
<button class="lang-active">FR</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="game-sidebar-section">
|
||||||
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path fill="currentColor" d="M304 70.1C313.1 61.9 326.9 61.9 336 70.1L568 278.1C577.9 286.9 578.7 302.1 569.8 312C560.9 321.9 545.8 322.7 535.9 313.8L527.9 306.6L527.9 511.9C527.9 547.2 499.2 575.9 463.9 575.9L175.9 575.9C140.6 575.9 111.9 547.2 111.9 511.9L111.9 306.6L103.9 313.8C94 322.6 78.9 321.8 70 312C61.1 302.2 62 287 71.8 278.1L304 70.1zM320 120.2L160 263.7L160 512C160 520.8 167.2 528 176 528L224 528L224 424C224 384.2 256.2 352 296 352L344 352C383.8 352 416 384.2 416 424L416 528L464 528C472.8 528 480 520.8 480 512L480 263.7L320 120.3zM272 528L368 528L368 424C368 410.7 357.3 400 344 400L296 400C282.7 400 272 410.7 272 424L272 528z"></path></svg>
|
||||||
|
<a href="#" class="game-sidebar-link">Nouvelle partie</a>
|
||||||
|
</div>
|
||||||
|
<div class="game-sidebar-section">
|
||||||
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path fill="currentColor" d="M416 160L480 160C497.7 160 512 174.3 512 192L512 448C512 465.7 497.7 480 480 480L416 480C398.3 480 384 494.3 384 512C384 529.7 398.3 544 416 544L480 544C533 544 576 501 576 448L576 192C576 139 533 96 480 96L416 96C398.3 96 384 110.3 384 128C384 145.7 398.3 160 416 160zM406.6 342.6C419.1 330.1 419.1 309.8 406.6 297.3L278.6 169.3C266.1 156.8 245.8 156.8 233.3 169.3C220.8 181.8 220.8 202.1 233.3 214.6L306.7 288L96 288C78.3 288 64 302.3 64 320C64 337.7 78.3 352 96 352L306.7 352L233.3 425.4C220.8 437.9 220.8 458.2 233.3 470.7C245.8 483.2 266.1 483.2 278.6 470.7L406.6 342.7z"></path></svg>
|
||||||
|
<a href="#" class="game-sidebar-link">Se connecter</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div><span class="site-nav-version">v0.2.15</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Portal behind the overlay ─────────────────────────────────────── -->
|
||||||
|
<main>
|
||||||
|
<div style="display:flex;justify-content:center;align-items:flex-start;padding-top:5vh" class="portal-main">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="login-card-header"><div class="login-board-stripe"></div></div>
|
||||||
|
<div class="login-card-body">
|
||||||
|
<h1 class="login-title">Trictrac</h1>
|
||||||
|
<p class="login-subtitle"><em>Une interprétation numérique</em></p>
|
||||||
|
<div class="login-ornament">✦</div>
|
||||||
|
<div class="login-actions">
|
||||||
|
<button class="login-btn login-btn-secondary">Jouer contre le bot</button>
|
||||||
|
<button class="login-btn login-btn-primary">Inviter un adversaire</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════════
|
||||||
|
GAME OVERLAY
|
||||||
|
══════════════════════════════════════════════════════════════════════ -->
|
||||||
|
<div class="game-overlay">
|
||||||
|
<div class="game-container">
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════════════════════════════
|
||||||
|
PLAYER STRIP
|
||||||
|
In-flow (scrolls with page). Reaches full viewport width via
|
||||||
|
width:100vw + margin-left:calc(50%-50vw).
|
||||||
|
Touches viewport top via margin-top:-1.5rem.
|
||||||
|
|
||||||
|
Left half DOM order: [avatar][name][pegs][pts] → justify:flex-end
|
||||||
|
Right half DOM order: [pts][pegs][name][avatar] → justify:flex-start
|
||||||
|
Both: pts-counter is the item closest to VS.
|
||||||
|
════════════════════════════════════════════════════════════════ -->
|
||||||
|
<div class="v07-strip">
|
||||||
|
|
||||||
|
<!-- Left: me (active) ──────────────────────────────────────────── -->
|
||||||
|
<div class="v07-player v07-player-left">
|
||||||
|
<div class="v07-active-zone active">
|
||||||
|
<!-- avatar outermost (leftmost) -->
|
||||||
|
<div class="v07-avatar v07-avatar-me">A</div>
|
||||||
|
<!-- name -->
|
||||||
|
<div class="score-row-name">
|
||||||
|
<span class="player-name">Anonyme</span>
|
||||||
|
<span class="you-tag">(vous)</span>
|
||||||
|
</div>
|
||||||
|
<!-- peg track -->
|
||||||
|
<div class="peg-track">
|
||||||
|
<div class="peg-hole filled"></div>
|
||||||
|
<div class="peg-hole filled"></div>
|
||||||
|
<div class="peg-hole filled"></div>
|
||||||
|
<div class="peg-hole filled"></div>
|
||||||
|
<div class="peg-hole filled"></div>
|
||||||
|
<div class="peg-hole filled"></div>
|
||||||
|
<div class="peg-hole"></div><div class="peg-hole"></div>
|
||||||
|
<div class="peg-hole"></div><div class="peg-hole"></div>
|
||||||
|
<div class="peg-hole"></div><div class="peg-hole"></div>
|
||||||
|
</div>
|
||||||
|
<!-- score: closest to VS -->
|
||||||
|
<div class="pts-counter-wrap">
|
||||||
|
<div class="pts-counter-row">
|
||||||
|
<span class="pts-counter">6</span>
|
||||||
|
<span class="pts-max">/12</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.v07-player-left -->
|
||||||
|
|
||||||
|
<!-- Center: title ──────────────────────────────────────────────── -->
|
||||||
|
<div class="v07-strip-center">
|
||||||
|
<span class="v07-title">Trictrac</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: opp (not active) ────────────────────────────────────── -->
|
||||||
|
<div class="v07-player v07-player-right">
|
||||||
|
<div class="v07-active-zone">
|
||||||
|
<!-- score: closest to VS -->
|
||||||
|
<div class="pts-counter-wrap">
|
||||||
|
<div class="pts-counter-row">
|
||||||
|
<span class="pts-counter">2</span>
|
||||||
|
<span class="pts-max">/12</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- peg track -->
|
||||||
|
<div class="peg-track">
|
||||||
|
<div class="peg-hole peg-opp filled"></div>
|
||||||
|
<div class="peg-hole peg-opp filled"></div>
|
||||||
|
<div class="peg-hole peg-opp"></div><div class="peg-hole peg-opp"></div>
|
||||||
|
<div class="peg-hole peg-opp"></div><div class="peg-hole peg-opp"></div>
|
||||||
|
<div class="peg-hole peg-opp"></div><div class="peg-hole peg-opp"></div>
|
||||||
|
<div class="peg-hole peg-opp"></div><div class="peg-hole peg-opp"></div>
|
||||||
|
<div class="peg-hole peg-opp"></div><div class="peg-hole peg-opp"></div>
|
||||||
|
</div>
|
||||||
|
<!-- name -->
|
||||||
|
<div class="score-row-name">
|
||||||
|
<span class="player-name">Bot</span>
|
||||||
|
</div>
|
||||||
|
<!-- avatar outermost (rightmost) -->
|
||||||
|
<div class="v07-avatar v07-avatar-opp">B</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.v07-player-right -->
|
||||||
|
|
||||||
|
</div><!-- /.v07-strip -->
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════════════════════════════
|
||||||
|
BODY — [board] [sidebar (wide only)]
|
||||||
|
════════════════════════════════════════════════════════════════ -->
|
||||||
|
<div class="v07-body">
|
||||||
|
|
||||||
|
<!-- Board ─────────────────────────────────────────────────────── -->
|
||||||
|
<div class="board-wrapper">
|
||||||
|
<div class="board">
|
||||||
|
|
||||||
|
<!-- top row ────────────────────────────────────────────────── -->
|
||||||
|
<div class="board-row top-row">
|
||||||
|
<div class="board-quarter">
|
||||||
|
<div class="field zone-opponent corner" id="field-13"><span class="field-num">13</span></div>
|
||||||
|
<div class="field zone-opponent" id="field-14"><span class="field-num">14</span></div>
|
||||||
|
<div class="field zone-opponent" id="field-15">
|
||||||
|
<span class="field-num">15</span>
|
||||||
|
<div class="checker-stack">
|
||||||
|
<div class="checker black"></div><div class="checker black"></div><div class="checker black"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field zone-opponent" id="field-16"><span class="field-num">16</span></div>
|
||||||
|
<div class="field zone-opponent" id="field-17"><span class="field-num">17</span></div>
|
||||||
|
<div class="field zone-opponent" id="field-18"><span class="field-num">18</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="board-bar"></div>
|
||||||
|
<div class="board-quarter">
|
||||||
|
<div class="field zone-retour" id="field-19">
|
||||||
|
<span class="field-num">19</span>
|
||||||
|
<div class="hit-ripple hit-ripple-top"></div>
|
||||||
|
<div class="checker-stack"><div class="checker black"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="field zone-retour" id="field-20"><span class="field-num">20</span></div>
|
||||||
|
<div class="field zone-retour" id="field-21"><span class="field-num">21</span></div>
|
||||||
|
<div class="field zone-retour" id="field-22"><span class="field-num">22</span></div>
|
||||||
|
<div class="field zone-retour point-no-bredouille" id="field-23"><span class="field-num">23</span></div>
|
||||||
|
<div class="field zone-retour point-no-bredouille" id="field-24">
|
||||||
|
<span class="field-num">24</span>
|
||||||
|
<div class="checker-stack">
|
||||||
|
<div class="checker black"></div><div class="checker black"></div>
|
||||||
|
<div class="checker black"></div><div class="checker black">11</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="board-center-bar"></div>
|
||||||
|
|
||||||
|
<!-- bottom row ─────────────────────────────────────────────── -->
|
||||||
|
<div class="board-row bot-row">
|
||||||
|
<div class="board-quarter">
|
||||||
|
<div class="field zone-grand corner corner-available clickable dest" id="field-12"
|
||||||
|
title="Coin de repos — must enter and leave with 2 checkers">
|
||||||
|
<span class="field-num">12</span>
|
||||||
|
</div>
|
||||||
|
<div class="field zone-grand clickable dest" id="field-11"><span class="field-num">11</span></div>
|
||||||
|
<div class="field zone-grand" id="field-10"><span class="field-num">10</span></div>
|
||||||
|
<div class="field zone-grand" id="field-9">
|
||||||
|
<span class="field-num">9</span>
|
||||||
|
<div class="checker-stack"><div class="checker white"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="field zone-grand" id="field-8">
|
||||||
|
<span class="field-num">8</span>
|
||||||
|
<div class="checker-stack"><div class="checker white"></div><div class="checker white"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="field zone-grand selected clickable" id="field-7">
|
||||||
|
<span class="field-num">7</span>
|
||||||
|
<div class="checker-stack"><div class="checker white"></div><div class="checker white"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="board-bar"></div>
|
||||||
|
<div class="board-quarter">
|
||||||
|
<div class="field zone-petit point-no-bredouille" id="field-6"><span class="field-num">6</span></div>
|
||||||
|
<div class="field zone-petit point-no-bredouille" id="field-5"><span class="field-num">5</span></div>
|
||||||
|
<div class="field zone-petit point-no-bredouille" id="field-4"><span class="field-num">4</span></div>
|
||||||
|
<div class="field zone-petit point-no-bredouille" id="field-3"><span class="field-num">3</span></div>
|
||||||
|
<div class="field zone-petit point-no-bredouille" id="field-2"><span class="field-num">2</span></div>
|
||||||
|
<div class="field zone-petit point-no-bredouille" id="field-1">
|
||||||
|
<span class="field-num">1</span>
|
||||||
|
<div class="checker-stack">
|
||||||
|
<div class="checker white">10</div><div class="checker white"></div>
|
||||||
|
<div class="checker white"></div><div class="checker white"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svg style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible" width="761" height="388"></svg>
|
||||||
|
|
||||||
|
</div><!-- /.board -->
|
||||||
|
</div><!-- /.board-wrapper -->
|
||||||
|
|
||||||
|
<!-- Right sidebar (wide ≥920px) ─────────────────────────────── -->
|
||||||
|
<div class="v07-sidebar">
|
||||||
|
|
||||||
|
<div class="v07-sidebar-dice">
|
||||||
|
<div class="v07-sidebar-dice-row">
|
||||||
|
<svg class="die-face" width="48" height="48" viewBox="0 0 48 48">
|
||||||
|
<rect x="1.5" y="1.5" width="45" height="45" rx="7" ry="7"></rect>
|
||||||
|
<circle cx="13" cy="13" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="13" r="4.5"></circle>
|
||||||
|
<circle cx="13" cy="24" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="24" r="4.5"></circle>
|
||||||
|
<circle cx="13" cy="35" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="35" r="4.5"></circle>
|
||||||
|
</svg>
|
||||||
|
<svg class="die-face die-used" width="48" height="48" viewBox="0 0 48 48">
|
||||||
|
<rect x="1.5" y="1.5" width="45" height="45" rx="7" ry="7"></rect>
|
||||||
|
<circle cx="13" cy="13" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="13" r="4.5"></circle>
|
||||||
|
<circle cx="13" cy="35" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="35" r="4.5"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<label class="free-mode-toggle">
|
||||||
|
<input type="checkbox">Mode jeu libre
|
||||||
|
<span class="free-mode-help" title="Sélectionnez n'importe quelle dame et tentez de trouver un coup valide vous-même.">?</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="v07-sidebar-status">
|
||||||
|
<div class="game-status">Déplacez une dame (1 sur 2)</div>
|
||||||
|
<div class="board-actions">
|
||||||
|
<button class="btn btn-secondary">Passer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /.v07-sidebar -->
|
||||||
|
|
||||||
|
</div><!-- /.v07-body -->
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════════════════════════════
|
||||||
|
BOTTOM BAR (narrow <920px) — two rounded cards, 0.5rem gap
|
||||||
|
════════════════════════════════════════════════════════════════ -->
|
||||||
|
<div class="v07-bottom-bar">
|
||||||
|
|
||||||
|
<div class="v07-foot-dice">
|
||||||
|
<div class="v07-foot-dice-row">
|
||||||
|
<svg class="die-face" width="44" height="44" viewBox="0 0 48 48">
|
||||||
|
<rect x="1.5" y="1.5" width="45" height="45" rx="7" ry="7"></rect>
|
||||||
|
<circle cx="13" cy="13" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="13" r="4.5"></circle>
|
||||||
|
<circle cx="13" cy="24" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="24" r="4.5"></circle>
|
||||||
|
<circle cx="13" cy="35" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="35" r="4.5"></circle>
|
||||||
|
</svg>
|
||||||
|
<svg class="die-face die-used" width="44" height="44" viewBox="0 0 48 48">
|
||||||
|
<rect x="1.5" y="1.5" width="45" height="45" rx="7" ry="7"></rect>
|
||||||
|
<circle cx="13" cy="13" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="13" r="4.5"></circle>
|
||||||
|
<circle cx="13" cy="35" r="4.5"></circle>
|
||||||
|
<circle cx="35" cy="35" r="4.5"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<label class="free-mode-toggle">
|
||||||
|
<input type="checkbox">Mode libre
|
||||||
|
<span class="free-mode-help" title="Sélectionnez n'importe quelle dame et tentez de trouver un coup valide vous-même.">?</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="v07-foot-main">
|
||||||
|
<div class="game-status">Déplacez une dame (1 sur 2)</div>
|
||||||
|
<div class="board-actions">
|
||||||
|
<button class="btn btn-secondary">Passer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /.v07-bottom-bar -->
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════════════════════════════
|
||||||
|
SCORING ROW — always below controls
|
||||||
|
════════════════════════════════════════════════════════════════ -->
|
||||||
|
<div class="v07-scoring-row">
|
||||||
|
<div class="scoring-panels-container">
|
||||||
|
<div class="scoring-panel-wrapper">
|
||||||
|
<button class="scoring-expand-btn" title="Afficher le détail">+</button>
|
||||||
|
<div class="scoring-panel">
|
||||||
|
<div class="scoring-panel-head">
|
||||||
|
<div class="scoring-total">+4 pts</div>
|
||||||
|
<button class="scoring-collapse-btn">−</button>
|
||||||
|
</div>
|
||||||
|
<div class="scoring-jan-row">
|
||||||
|
<span class="jan-label">Battage à vrai (petit jan)</span>
|
||||||
|
<span class="jan-tag">simple</span>
|
||||||
|
<span class="jan-tag">×1</span>
|
||||||
|
<span class="jan-pts">+4</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.v07-scoring-row -->
|
||||||
|
|
||||||
|
</div><!-- /.game-container -->
|
||||||
|
</div><!-- /.game-overlay -->
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue