fix: show login name in game
This commit is contained in:
parent
15a2963f7e
commit
c46d26ae02
7 changed files with 81 additions and 47 deletions
|
|
@ -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%),
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue