fix: integrate multiplayer
This commit is contained in:
parent
3f3f4598f6
commit
82803ded36
10 changed files with 75 additions and 2308 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,3 +17,4 @@ client_web/dist
|
||||||
var
|
var
|
||||||
|
|
||||||
deploy
|
deploy
|
||||||
|
clients/**/dist
|
||||||
|
|
|
||||||
|
|
@ -1194,3 +1194,20 @@ body {
|
||||||
color: var(--ui-red-accent);
|
color: var(--ui-red-accent);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.auth-badge {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.auth-badge--in { background: rgba(96,165,250,0.15); color: #93c5fd; }
|
||||||
|
.auth-badge--out { background: rgba(148,163,184,0.1); color: #64748b; }
|
||||||
|
.auth-badge a { color: #60a5fa; }
|
||||||
|
|
||||||
|
.playing-as {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #64748b;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
|
||||||
1173
clients/web-game/dist/client_web-4248a2b78bb5a03.js
vendored
1173
clients/web-game/dist/client_web-4248a2b78bb5a03.js
vendored
File diff suppressed because it is too large
Load diff
Binary file not shown.
1133
clients/web-game/dist/style-398501cc5e039e60.css
vendored
1133
clients/web-game/dist/style-398501cc5e039e60.css
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -19,7 +19,7 @@ use trictrac_store::CheckerMove;
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
const RELAY_URL: &str = "ws://127.0.0.1:8080/ws";
|
const RELAY_URL: &str = "ws://localhost:8080/ws";
|
||||||
const GAME_ID: &str = "trictrac";
|
const GAME_ID: &str = "trictrac";
|
||||||
const STORAGE_KEY: &str = "trictrac_session";
|
const STORAGE_KEY: &str = "trictrac_session";
|
||||||
|
|
||||||
|
|
@ -152,6 +152,23 @@ pub fn App() -> impl IntoView {
|
||||||
};
|
};
|
||||||
let screen = RwSignal::new(initial_screen);
|
let screen = RwSignal::new(initial_screen);
|
||||||
|
|
||||||
|
// Auth: fetch once and expose to all child components via context.
|
||||||
|
let auth_username: RwSignal<Option<String>> = RwSignal::new(None);
|
||||||
|
provide_context(auth_username);
|
||||||
|
spawn_local(async move {
|
||||||
|
if let Ok(resp) = gloo_net::http::Request::get(&format!("{HTTP_BASE}/auth/me"))
|
||||||
|
.credentials(web_sys::RequestCredentials::Include)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if resp.status() == 200 {
|
||||||
|
if let Ok(me) = resp.json::<MeResponse>().await {
|
||||||
|
auth_username.set(Some(me.username));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let (cmd_tx, mut cmd_rx) = mpsc::unbounded::<NetCommand>();
|
let (cmd_tx, mut cmd_rx) = mpsc::unbounded::<NetCommand>();
|
||||||
let pending: RwSignal<VecDeque<GameUiState>> = RwSignal::new(VecDeque::new());
|
let pending: RwSignal<VecDeque<GameUiState>> = RwSignal::new(VecDeque::new());
|
||||||
provide_context(pending);
|
provide_context(pending);
|
||||||
|
|
@ -275,6 +292,7 @@ pub fn App() -> impl IntoView {
|
||||||
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 mut vs = ViewState::default_with_names("Blancs", "Noirs");
|
||||||
|
let mut result_submitted = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
futures::select! {
|
futures::select! {
|
||||||
|
|
@ -297,6 +315,15 @@ 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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host reports outcomes once per terminal game state.
|
||||||
|
if is_host && !result_submitted && vs.stage == SerStage::Ended {
|
||||||
|
result_submitted = true;
|
||||||
|
let room = room_id_for_storage.clone();
|
||||||
|
let gs = vs.clone();
|
||||||
|
spawn_local(submit_game_result(room, gs));
|
||||||
|
}
|
||||||
|
|
||||||
if is_host {
|
if is_host {
|
||||||
save_session(&StoredSession {
|
save_session(&StoredSession {
|
||||||
relay_url: RELAY_URL.to_string(),
|
relay_url: RELAY_URL.to_string(),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ use super::scoring::ScoringPanel;
|
||||||
pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
|
|
||||||
|
let auth_username =
|
||||||
|
use_context::<RwSignal<Option<String>>>().expect("auth_username not found in context");
|
||||||
let vs = state.view_state.clone();
|
let vs = state.view_state.clone();
|
||||||
let player_id = state.player_id;
|
let player_id = state.player_id;
|
||||||
let is_my_turn = vs.active_mp_player == Some(player_id);
|
let is_my_turn = vs.active_mp_player == Some(player_id);
|
||||||
|
|
@ -240,6 +242,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
on:click=move |_| i18n.set_locale(Locale::fr)
|
on:click=move |_| i18n.set_locale(Locale::fr)
|
||||||
>"FR"</button>
|
>"FR"</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{move || auth_username.get().map(|u| view! {
|
||||||
|
<p class="playing-as">"Playing as " <strong>{u}</strong></p>
|
||||||
|
})}
|
||||||
|
|
||||||
<a class="quit-link" href="#" on:click=move |e| {
|
<a class="quit-link" href="#" on:click=move |e| {
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
cmd_tx_quit.unbounded_send(NetCommand::Disconnect).ok();
|
cmd_tx_quit.unbounded_send(NetCommand::Disconnect).ok();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ use leptos::prelude::*;
|
||||||
use crate::app::NetCommand;
|
use crate::app::NetCommand;
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const PORTAL_URL: &str = "http://localhost:9092";
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const PORTAL_URL: &str = "/portal";
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn LoginScreen(error: Option<String>) -> impl IntoView {
|
pub fn LoginScreen(error: Option<String>) -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
|
|
@ -11,6 +16,8 @@ pub fn LoginScreen(error: Option<String>) -> impl IntoView {
|
||||||
|
|
||||||
let cmd_tx = use_context::<UnboundedSender<NetCommand>>()
|
let cmd_tx = use_context::<UnboundedSender<NetCommand>>()
|
||||||
.expect("UnboundedSender<NetCommand> not found in context");
|
.expect("UnboundedSender<NetCommand> not found in context");
|
||||||
|
let auth_username =
|
||||||
|
use_context::<RwSignal<Option<String>>>().expect("auth_username not found in context");
|
||||||
|
|
||||||
let cmd_tx_create = cmd_tx.clone();
|
let cmd_tx_create = cmd_tx.clone();
|
||||||
let cmd_tx_join = cmd_tx.clone();
|
let cmd_tx_join = cmd_tx.clone();
|
||||||
|
|
@ -47,6 +54,19 @@ pub fn LoginScreen(error: Option<String>) -> impl IntoView {
|
||||||
|
|
||||||
{error.map(|err| view! { <p class="error-msg">{err}</p> })}
|
{error.map(|err| view! { <p class="error-msg">{err}</p> })}
|
||||||
|
|
||||||
|
// Auth status badge
|
||||||
|
{move || match auth_username.get() {
|
||||||
|
Some(u) => view! {
|
||||||
|
<p class="auth-badge auth-badge--in">"✓ Logged in as " <strong>{u}</strong></p>
|
||||||
|
}.into_any(),
|
||||||
|
None => view! {
|
||||||
|
<p class="auth-badge auth-badge--out">
|
||||||
|
"Not logged in — games won't be tracked. "
|
||||||
|
<a href=PORTAL_URL target="_blank">"Create account"</a>
|
||||||
|
</p>
|
||||||
|
}.into_any(),
|
||||||
|
}}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="login-input"
|
class="login-input"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ mod inner {
|
||||||
/// Play the pre-recorded dice-roll MP3 asset.
|
/// Play the pre-recorded dice-roll MP3 asset.
|
||||||
pub fn play_dice_roll() {
|
pub fn play_dice_roll() {
|
||||||
if let Ok(audio) = web_sys::HtmlAudioElement::new_with_src("/diceroll.mp3") {
|
if let Ok(audio) = web_sys::HtmlAudioElement::new_with_src("/diceroll.mp3") {
|
||||||
|
audio.set_volume(0.2);
|
||||||
let _ = audio.play();
|
let _ = audio.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
justfile
2
justfile
|
|
@ -41,7 +41,7 @@ build-relay:
|
||||||
CARGO_PROFILE_RELEASE_OPT_LEVEL=3 cargo build -p relay-server --release
|
CARGO_PROFILE_RELEASE_OPT_LEVEL=3 cargo build -p relay-server --release
|
||||||
mkdir -p deploy
|
mkdir -p deploy
|
||||||
cp target/release/relay-server deploy
|
cp target/release/relay-server deploy
|
||||||
cp -u service/relay-server/GameConfig.json deploy/
|
cp -u server/relay-server/GameConfig.json deploy/
|
||||||
|
|
||||||
runclibots:
|
runclibots:
|
||||||
cargo run --bin=client_cli -- --bot random,dqnburn:./bot/models/burnrl_dqn_40.mpk
|
cargo run --bin=client_cli -- --bot random,dqnburn:./bot/models/burnrl_dqn_40.mpk
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue