fix: show login name in game

This commit is contained in:
Henri Bourcereau 2026-04-25 21:51:16 +02:00
parent 15a2963f7e
commit c46d26ae02
7 changed files with 81 additions and 47 deletions

View file

@ -290,7 +290,7 @@ a:hover { text-decoration: underline; }
/* ── Game overlay (full-screen, covers portal during play) ───────── */ /* ── Game overlay (full-screen, covers portal during play) ───────── */
.game-overlay { .game-overlay {
position: fixed; position: fixed;
inset: 0; inset: 54px 0 0 0;
background: #8a7050; background: #8a7050;
background-image: background-image:
radial-gradient(ellipse at 20% 10%, rgba(80,48,16,0.35) 0%, transparent 60%), radial-gradient(ellipse at 20% 10%, rgba(80,48,16,0.35) 0%, transparent 60%),

View file

@ -58,6 +58,7 @@
"hint_move": "Click a highlighted field to move a checker", "hint_move": "Click a highlighted field to move a checker",
"hint_hold_or_go": "Hold to keep points — Go to reset the setting", "hint_hold_or_go": "Hold to keep points — Go to reset the setting",
"hint_continue": "Click Continue when ready", "hint_continue": "Click Continue when ready",
"anonymous_name": "Anonymous",
"login_failed": "Invalid username or password.", "login_failed": "Invalid username or password.",
"sign_in": "Sign in", "sign_in": "Sign in",
"sign_out": "Sign out", "sign_out": "Sign out",

View file

@ -58,6 +58,7 @@
"hint_move": "Cliquez un champ surligné pour déplacer", "hint_move": "Cliquez un champ surligné pour déplacer",
"hint_hold_or_go": "Tenir pour garder les points — S'en aller pour repartir", "hint_hold_or_go": "Tenir pour garder les points — S'en aller pour repartir",
"hint_continue": "Cliquez Continuer quand vous êtes prêt", "hint_continue": "Cliquez Continuer quand vous êtes prêt",
"anonymous_name": "Anonyme",
"login_failed": "Identifiant ou mot de passe incorrect.", "login_failed": "Identifiant ou mot de passe incorrect.",
"sign_in": "Se connecter", "sign_in": "Se connecter",
"sign_out": "Se déconnecter", "sign_out": "Se déconnecter",

View file

@ -4,6 +4,7 @@ use gloo_storage::{LocalStorage, Storage};
use leptos::prelude::*; use leptos::prelude::*;
use leptos::task::spawn_local; use leptos::task::spawn_local;
use leptos_router::components::{Route, Router, Routes}; use leptos_router::components::{Route, Router, Routes};
use leptos_router::hooks::use_location;
use leptos_router::path; use leptos_router::path;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,15 +12,15 @@ use backbone_lib::session::{ConnectError, GameSession, RoomConfig, RoomRole, Ses
use backbone_lib::traits::ViewStateUpdate; use backbone_lib::traits::ViewStateUpdate;
use crate::api; use crate::api;
use crate::i18n::*;
use crate::game::components::{ConnectingScreen, GameScreen}; use crate::game::components::{ConnectingScreen, GameScreen};
use crate::game::session::{ use crate::game::session::{
compute_last_moves, push_or_show, run_local_bot_game, compute_last_moves, patch_player_name, push_or_show, run_local_bot_game,
}; };
use crate::game::trictrac::backend::TrictracBackend; use crate::game::trictrac::backend::TrictracBackend;
use crate::game::trictrac::types::{ use crate::game::trictrac::types::{
GameDelta, PlayerAction, ScoredEvent, SerStage, ViewState, GameDelta, PlayerAction, ScoredEvent, SerStage, ViewState,
}; };
use crate::i18n::I18nContextProvider;
use crate::nav::SiteNav; use crate::nav::SiteNav;
use crate::portal::{account::AccountPage, game_detail::GameDetailPage, lobby::LobbyPage, profile::ProfilePage}; use crate::portal::{account::AccountPage, game_detail::GameDetailPage, lobby::LobbyPage, profile::ProfilePage};
use trictrac_store::CheckerMove; use trictrac_store::CheckerMove;
@ -128,6 +129,7 @@ async fn submit_game_result(room_code: String, game_state: ViewState) {
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
let i18n = use_i18n();
let stored = load_session(); let stored = load_session();
let initial_screen = if stored.is_some() { let initial_screen = if stored.is_some() {
Screen::Connecting Screen::Connecting
@ -225,8 +227,10 @@ pub fn App() -> impl IntoView {
}; };
if remote_config.is_none() { if remote_config.is_none() {
let player_name = auth_username.get_untracked()
.unwrap_or_else(|| t_string!(i18n, anonymous_name).to_string());
loop { loop {
let restart = run_local_bot_game(screen, &mut cmd_rx, pending).await; let restart = run_local_bot_game(screen, &mut cmd_rx, pending, player_name.clone()).await;
if !restart { if !restart {
break; break;
} }
@ -266,7 +270,9 @@ pub fn App() -> impl IntoView {
let is_host = session.is_host; let is_host = session.is_host;
let player_id = session.player_id; let player_id = session.player_id;
let reconnect_token = session.reconnect_token; let reconnect_token = session.reconnect_token;
let mut vs = ViewState::default_with_names("Blancs", "Noirs"); let my_name = auth_username.get_untracked()
.unwrap_or_else(|| t_string!(i18n, anonymous_name).to_string());
let mut vs = ViewState::default_with_names("", "");
let mut result_submitted = false; let mut result_submitted = false;
loop { loop {
@ -290,6 +296,7 @@ pub fn App() -> impl IntoView {
ViewStateUpdate::Full(state) => vs = state, ViewStateUpdate::Full(state) => vs = state,
ViewStateUpdate::Incremental(delta) => vs.apply_delta(&delta), ViewStateUpdate::Incremental(delta) => vs.apply_delta(&delta),
} }
patch_player_name(&mut vs, player_id, &my_name);
if is_host && !result_submitted && vs.stage == SerStage::Ended { if is_host && !result_submitted && vs.stage == SerStage::Ended {
result_submitted = true; result_submitted = true;
@ -343,42 +350,52 @@ pub fn App() -> impl IntoView {
}); });
view! { view! {
<I18nContextProvider> <Router>
<Router> <SiteNav />
// Nav: hidden while game overlay is active
<SiteNav />
// Portal pages — always mounted for router stability <main>
<main> <Routes fallback=|| view! { <p class="portal-empty" style="padding:3rem;text-align:center">"Page not found."</p> }>
<Routes fallback=|| view! { <p class="portal-empty" style="padding:3rem;text-align:center">"Page not found."</p> }> <Route path=path!("/") view=LobbyPage />
<Route path=path!("/") view=LobbyPage /> <Route path=path!("/account") view=AccountPage />
<Route path=path!("/account") view=AccountPage /> <Route path=path!("/profile/:username") view=ProfilePage />
<Route path=path!("/profile/:username") view=ProfilePage /> <Route path=path!("/games/:id") view=GameDetailPage />
<Route path=path!("/games/:id") view=GameDetailPage /> </Routes>
</Routes> </main>
</main>
// Game overlay: fixed, covers portal during play <GameOverlay pending=pending screen=screen />
{move || { </Router>
let q = pending.get(); }
let front = q.front().cloned(); }
if let Some(state) = front {
return view! { /// Renders the full-screen game overlay, but only when the current route is "/".
<div class="game-overlay"><GameScreen state /></div> /// This lets the user navigate to profile/account pages while a game is running.
}.into_any(); #[component]
} fn GameOverlay(
match screen.get() { pending: RwSignal<VecDeque<GameUiState>>,
Screen::Playing(state) => view! { screen: RwSignal<Screen>,
<div class="game-overlay"><GameScreen state /></div> ) -> impl IntoView {
}.into_any(), let location = use_location();
Screen::Connecting => view! {
<div class="game-overlay"><ConnectingScreen /></div> move || {
}.into_any(), if location.pathname.get() != "/" {
_ => view! { }.into_any(), return view! { }.into_any();
} }
}} let q = pending.get();
</Router> let front = q.front().cloned();
</I18nContextProvider> if let Some(state) = front {
return view! {
<div class="game-overlay"><GameScreen state /></div>
}.into_any();
}
match screen.get() {
Screen::Playing(state) => view! {
<div class="game-overlay"><GameScreen state /></div>
}.into_any(),
Screen::Connecting => view! {
<div class="game-overlay"><ConnectingScreen /></div>
}.into_any(),
_ => view! { }.into_any(),
}
} }
} }

View file

@ -18,12 +18,13 @@ pub async fn run_local_bot_game(
screen: RwSignal<Screen>, screen: RwSignal<Screen>,
cmd_rx: &mut mpsc::UnboundedReceiver<NetCommand>, cmd_rx: &mut mpsc::UnboundedReceiver<NetCommand>,
pending: RwSignal<VecDeque<GameUiState>>, pending: RwSignal<VecDeque<GameUiState>>,
player_name: String,
) -> bool { ) -> bool {
let mut backend = TrictracBackend::new(0); let mut backend = TrictracBackend::new(0);
backend.player_arrival(0); backend.player_arrival(0);
backend.player_arrival(1); backend.player_arrival(1);
let mut vs = ViewState::default_with_names("You", "Bot"); let mut vs = ViewState::default_with_names(&player_name, "Bot");
for cmd in backend.drain_commands() { for cmd in backend.drain_commands() {
match cmd { match cmd {
BackendCommand::ResetViewState => { BackendCommand::ResetViewState => {
@ -35,6 +36,7 @@ pub async fn run_local_bot_game(
_ => {} _ => {}
} }
} }
patch_bot_names(&mut vs, &player_name);
screen.set(Screen::Playing(GameUiState { screen.set(Screen::Playing(GameUiState {
view_state: vs.clone(), view_state: vs.clone(),
player_id: 0, player_id: 0,
@ -58,6 +60,7 @@ pub async fn run_local_bot_game(
vs.apply_delta(&delta); vs.apply_delta(&delta);
} }
} }
patch_bot_names(&mut vs, &player_name);
let scored = compute_scored_event(&prev_vs, &vs, 0); let scored = compute_scored_event(&prev_vs, &vs, 0);
let opp_scored = compute_scored_event(&prev_vs, &vs, 1); let opp_scored = compute_scored_event(&prev_vs, &vs, 1);
screen.set(Screen::Playing(GameUiState { screen.set(Screen::Playing(GameUiState {
@ -86,6 +89,7 @@ pub async fn run_local_bot_game(
if let BackendCommand::Delta(delta) = cmd { if let BackendCommand::Delta(delta) = cmd {
let delta_prev_vs = vs.clone(); let delta_prev_vs = vs.clone();
vs.apply_delta(&delta); vs.apply_delta(&delta);
patch_bot_names(&mut vs, &player_name);
push_or_show( push_or_show(
&delta_prev_vs, &delta_prev_vs,
GameUiState { GameUiState {
@ -110,6 +114,17 @@ pub async fn run_local_bot_game(
} }
} }
/// Patches the player names in a ViewState after a backend delta (bot game: slot 0 = human, 1 = Bot).
pub fn patch_bot_names(vs: &mut ViewState, player_name: &str) {
vs.scores[0].name = player_name.to_string();
vs.scores[1].name = "Bot".to_string();
}
/// Patches the local player's name in a ViewState after a backend delta (multiplayer).
pub fn patch_player_name(vs: &mut ViewState, player_id: u16, name: &str) {
vs.scores[player_id as usize].name = name.to_string();
}
/// Returns the checker moves to animate when the board changed between two ViewStates. /// Returns the checker moves to animate when the board changed between two ViewStates.
pub fn compute_last_moves( pub fn compute_last_moves(
prev: &ViewState, prev: &ViewState,

View file

@ -7,8 +7,13 @@ mod nav;
mod portal; mod portal;
use app::App; use app::App;
use i18n::I18nContextProvider;
use leptos::prelude::*; use leptos::prelude::*;
fn main() { fn main() {
mount_to_body(|| view! { <App /> }) mount_to_body(|| view! {
<I18nContextProvider>
<App />
</I18nContextProvider>
})
} }

View file

@ -3,19 +3,14 @@ use leptos::task::spawn_local;
use leptos_router::components::A; use leptos_router::components::A;
use crate::api; use crate::api;
use crate::app::Screen;
use crate::i18n::*; use crate::i18n::*;
#[component] #[component]
pub fn SiteNav() -> impl IntoView { pub fn SiteNav() -> impl IntoView {
let i18n = use_i18n(); let i18n = use_i18n();
let screen = use_context::<RwSignal<Screen>>().expect("Screen context not found");
let auth_username = let auth_username =
use_context::<RwSignal<Option<String>>>().expect("auth_username context not found"); use_context::<RwSignal<Option<String>>>().expect("auth_username context not found");
let is_game_active =
move || !matches!(screen.get(), Screen::Login { .. });
let logout = move |_| { let logout = move |_| {
spawn_local(async move { spawn_local(async move {
let _ = api::post_logout().await; let _ = api::post_logout().await;
@ -24,7 +19,7 @@ pub fn SiteNav() -> impl IntoView {
}; };
view! { view! {
<nav class="site-nav" class:hidden=is_game_active> <nav class="site-nav">
<A href="/" attr:class="site-nav-brand">"Trictrac"</A> <A href="/" attr:class="site-nav-brand">"Trictrac"</A>
<div class="site-nav-spacer" /> <div class="site-nav-spacer" />
<div class="lang-switcher"> <div class="lang-switcher">