From 852973eddf074cd72d4b26eddf1390b83129b438 Mon Sep 17 00:00:00 2001 From: emilis Date: Wed, 18 Feb 2026 01:44:41 +0000 Subject: [PATCH] cancellation fixes, aura fixes, etc. --- style/main.scss | 13 ++- .../src/game/settings/settings_role.rs | 16 +--- werewolves-proto/src/game_test/mod.rs | 13 +-- werewolves-proto/src/message.rs | 1 + werewolves-proto/src/message/host.rs | 1 + werewolves/src/app.rs | 9 +- werewolves/src/app/pages/game.rs | 15 ++-- werewolves/src/app/pages/game/host.rs | 84 +++++++++++-------- .../src/app/pages/game/host/settings.rs | 80 +++++++++++++----- werewolves/src/app/pages/game/player.rs | 17 ++-- werewolves/src/lib.rs | 6 +- werewolves/src/server/lobby.rs | 17 ++-- werewolves/src/server/mod.rs | 1 + werewolves/src/server/runner.rs | 14 +++- 14 files changed, 177 insertions(+), 110 deletions(-) diff --git a/style/main.scss b/style/main.scss index 5b1fb85..18ea6a7 100644 --- a/style/main.scss +++ b/style/main.scss @@ -11,7 +11,7 @@ $village_border: color.change($village_color, $alpha: 1.0); $wolves_border: color.change($wolves_color, $alpha: 1.0); $intel_color: color.adjust($village_color, $hue: -30deg); $intel_border: color.change($intel_color, $alpha: 1.0); -$defensive_color: color.adjust($village_color, $hue: -60deg); +$defensive_color: rgba(0, 128, 32, 0.9); //color.adjust(rgba(0, 16, 128, 0.9), $hue: -60deg); $defensive_border: color.change($defensive_color, $alpha: 1.0); $offensive_color: color.adjust($village_color, $hue: 30deg); $offensive_border: color.change($offensive_color, $alpha: 1.0); @@ -446,6 +446,17 @@ dialog::backdrop { gap: 0.25ch; align-items: flex-start; + .missing { + word-break: normal; + user-select: none; + font-size: 0.75em; + color: rgb(128, 0, 0); + + &:hover { + color: rgb(255, 0, 0); + } + } + .setup-slot { color: rgba(255, 255, 255, 0.9); font-size: 1.5em; diff --git a/werewolves-proto/src/game/settings/settings_role.rs b/werewolves-proto/src/game/settings/settings_role.rs index 1b6cc5c..bb07ad7 100644 --- a/werewolves-proto/src/game/settings/settings_role.rs +++ b/werewolves-proto/src/game/settings/settings_role.rs @@ -26,6 +26,7 @@ use crate::{ aura::AuraTitle, character::{Character, CharacterId}, error::GameError, + id_impl, message::Identification, player::PlayerId, role::{Role, RoleTitle}, @@ -426,20 +427,7 @@ impl From for SetupRole { } } -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -pub struct SlotId(Uuid); - -impl SlotId { - pub fn new() -> Self { - Self(Uuid::new_v4()) - } -} - -impl Display for SlotId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(&self.0, f) - } -} +id_impl!(SlotId); #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SetupSlot { diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 20fc82f..588737b 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -24,7 +24,7 @@ mod time; use crate::{ character::{Character, CharacterId}, diedto::DiedToTitle, - error::GameError, + error::{GameError, ServerError}, game::{Game, GameOver, GameSettings, OrRandom, SetupRole, SetupSlot}, message::{ CharacterState, Identification, PublicIdentity, @@ -803,11 +803,12 @@ fn wolfpack_kill_all_targets_valid() { for (idx, target) in living_villagers.into_iter().enumerate() { let mut attempt = game.clone(); - if let ServerToHostMessage::Error(GameError::InvalidTarget) = attempt - .process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::MarkTarget(target.character_id), - ))) - .unwrap() + if let ServerToHostMessage::Error(ServerError::GameError(GameError::InvalidTarget)) = + attempt + .process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::MarkTarget(target.character_id), + ))) + .unwrap() { panic!("invalid target {target:?} at index [{idx}]"); } diff --git a/werewolves-proto/src/message.rs b/werewolves-proto/src/message.rs index c26968d..ff3485a 100644 --- a/werewolves-proto/src/message.rs +++ b/werewolves-proto/src/message.rs @@ -66,6 +66,7 @@ pub struct DayCharacter { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)] pub enum ServerToClientMessage { + GameCancelled, Disconnect, LobbyInfo { joined: bool, diff --git a/werewolves-proto/src/message/host.rs b/werewolves-proto/src/message/host.rs index b26d063..4d89aad 100644 --- a/werewolves-proto/src/message/host.rs +++ b/werewolves-proto/src/message/host.rs @@ -93,6 +93,7 @@ pub enum HostLobbyMessage { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, werewolves_macros::Titles)] pub enum ServerToHostMessage { + GameCancelled, Disconnect, Daytime { characters: Box<[CharacterState]>, diff --git a/werewolves/src/app.rs b/werewolves/src/app.rs index aa65cf1..d5f64b4 100644 --- a/werewolves/src/app.rs +++ b/werewolves/src/app.rs @@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize}; use crate::{ app::{ - components::Nav, + components::{ErrorBox, Nav}, pages::{GamePage, Main, NotFound, Signin, Signup, UserSettings}, storage::{ Stored, @@ -96,6 +96,7 @@ pub fn App() -> impl IntoView { .then_some(auth_store.session().get().is_some()) }; let not_logged_in = move || Some(auth_store.session().get().is_none()); + let error = RwSignal::new(None); view! { @@ -106,6 +107,7 @@ pub fn App() -> impl IntoView {
diff --git a/werewolves/src/app/pages/game.rs b/werewolves/src/app/pages/game.rs index bc4e072..08568bb 100644 --- a/werewolves/src/app/pages/game.rs +++ b/werewolves/src/app/pages/game.rs @@ -6,16 +6,14 @@ use codee::binary::MsgpackSerdeCodec; use leptos::prelude::*; use leptos_router::hooks; use leptos_use::{ - ReconnectLimit, UseWebSocketOptions, - UseWebSocketReturn, core::ConnectionReadyState, use_websocket_with_options, + ReconnectLimit, UseWebSocketOptions, UseWebSocketReturn, core::ConnectionReadyState, + use_websocket_with_options, }; use reactive_stores::Store; -use werewolves_proto::message::{ - ClientMessage, - host::HostMessage, -}; +use werewolves_proto::message::{ClientMessage, host::HostMessage}; use werewolves_proto::message::{IntoClientResponse, WrappedServerMessage}; +use crate::app::components::ErrorBox; use crate::{ ConsoleLogError, app::{ @@ -27,7 +25,7 @@ use crate::{ }; #[component] -pub fn GamePage() -> impl IntoView { +pub fn GamePage(error: WriteSignal>) -> impl IntoView { move || { let params = hooks::use_params_map(); let auth = expect_context::>(); @@ -186,8 +184,9 @@ pub fn GamePage() -> impl IntoView { view! { {status} - + >, message: Signal>, reply: WriteSignal>, ) -> impl IntoView { let prefs = expect_context::<(Signal, WriteSignal)>().0; + let page = RwSignal::new(HostPage::default()); let settings = RwSignal::new(GameSettings::default()); let qr_mode = RwSignal::new(false); let players: RwSignal> = RwSignal::new(Box::new([])); - let dialog_open = RwSignal::new(false); + let dialog_open = RwSignal::new(HashMap::new()); let open_categories = RwSignal::new( Category::ALL .into_iter() @@ -32,21 +40,12 @@ pub fn HostGamePage( .collect::>(), ); - Effect::watch( - move || settings.get(), - move |s: &GameSettings, _, _| { - reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetGameSettings( - s.clone(), - )))); - }, - false, - ); Effect::watch( move || qr_mode.get(), move |q, _, _| reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetQrMode(*q)))), false, ); - let content = move || { + Effect::new(move || { if let Some(message) = message.get() { match message { Srv2Host::Lobby { @@ -54,32 +53,50 @@ pub fn HostGamePage( settings: s, qr_mode: q, } => { - log::info!("setting setties"); + page.set(HostPage::Settings); settings.set(s); *qr_mode.write_untracked() = q; players.set(p); - - view! { - + { + let mut d = dialog_open.write(); + for slot in settings.read().slots().iter() { + d.entry(slot.slot_id).or_insert(false); + } } - .into_any() } - _ => view! {

{format!("{message:#?}")}

}.into_any(), + Srv2Host::GameCancelled => { + error.set(Some("game was cancelled".into())); + gloo::utils::window() + .location() + .replace("/") + .console_log_warn(); + } + Srv2Host::Error(err) => { + error.set(Some(err.to_string())); + } + _ => log::error!("{message:#?}"), } - } else { - ().into_any() } - }; + }); let cancel = move || { - view! { - + message + .get() + .is_some() + .then_some(view! { }) + }; + let content = move || match page.get() { + HostPage::None => ().into_any(), + HostPage::Settings => view! { + } + .into_any(), }; view! { {cancel} @@ -96,13 +113,6 @@ fn CancelGame( let cancel = move |_| { open.set(false); reply.set(Some(HostMessage::CancelGame)); - #[cfg(not(feature = "ssr"))] - { - gloo::utils::window() - .location() - .replace("/") - .console_log_warn(); - } }; let derive_hidden = RwSignal::new(false); Effect::new(move || derive_hidden.set(!prefs.get().show_cancel_game)); @@ -123,5 +133,5 @@ fn CancelGame( } }; - view! { {content} } + move || view! { {content} } } diff --git a/werewolves/src/app/pages/game/host/settings.rs b/werewolves/src/app/pages/game/host/settings.rs index e2b64b7..714d0f6 100644 --- a/werewolves/src/app/pages/game/host/settings.rs +++ b/werewolves/src/app/pages/game/host/settings.rs @@ -5,8 +5,11 @@ use convert_case::{Case, Casing}; use leptos::{ev::MouseEvent, prelude::*}; use werewolves_proto::{ aura::AuraTitle, - game::{Category, GameSettings, SetupSlot}, - message::PlayerState, + game::{Category, GameSettings, SetupSlot, SlotId}, + message::{ + PlayerState, + host::{HostLobbyMessage, HostMessage}, + }, role::RoleTitle, }; @@ -18,11 +21,12 @@ use crate::app::{ #[component] pub fn Settings( - settings: RwSignal, + settings: ReadSignal, players: ReadSignal>, qr_mode: RwSignal, - dialog_open: RwSignal, + dialog_open: RwSignal>, open_categories: RwSignal>, + reply: WriteSignal>, ) -> impl IntoView { let slots = move || { settings @@ -32,11 +36,11 @@ pub fn Settings( .cloned() .map(move |s| { let signal = RwSignal::new(s); - Effect::watch( - move || signal.get(), - move |slot_update, _, _| settings.write().update_slot(slot_update.clone()), - false, - ); + Effect::new(move || { + let mut s = settings.get(); + s.update_slot(signal.get()); + reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(s)))); + }); view! { } }) @@ -68,7 +72,6 @@ pub fn Settings( k.sort(); k }; - Effect::new(|| log::debug!("rendering settings")); let categories = ordered_keys .into_iter() .map(|c| { @@ -85,7 +88,11 @@ pub fn Settings( .map(|r| { let add_role = move |ev: MouseEvent| { ev.prevent_default(); - settings.write().new_slot(r); + let mut s = settings.get(); + s.new_slot(r); + reply.set(Some(HostMessage::Lobby( + HostLobbyMessage::SetGameSettings(s), + ))); }; let classes = ["add-role", r.class(), "faint", "hover", "box"].as_classes(); @@ -129,7 +136,7 @@ pub fn Settings( fn SettingsSetupSlot( setup_slot: RwSignal, players: ReadSignal>, - dialog_open: RwSignal, + dialog_open: RwSignal>, ) -> impl IntoView { let auras = move || { let slot = setup_slot.read(); @@ -158,18 +165,42 @@ fn SettingsSetupSlot( } .into_any() } - None => { - view! { "missing player "{a.to_string()} } - .into_any() - } + None => view! { "assigned player not in lobby" } + .into_any(), } }) }; + let open_signal = RwSignal::new(false); + Effect::new(move || { + open_signal.set( + dialog_open + .read() + .get(&setup_slot.read().slot_id) + .copied() + .unwrap_or_default(), + ); + }); + Effect::new(move || { + let current = dialog_open + .read_untracked() + .get(&setup_slot.read_untracked().slot_id) + .copied() + .unwrap_or_default(); + let new = open_signal.get(); + if current == new { + return; + } + + dialog_open + .write() + .insert(setup_slot.read_untracked().slot_id, new); + }); + move || { view! {
) -> impl IntoView { ev.prevent_default(); let mut slot = setup_slot.write(); if slot.auras.contains(&aura) { + log::debug!("removing aura {aura}"); slot.auras.retain(|a| aura != *a); } else { + log::debug!("adding aura {aura}"); slot.auras.push(aura); } }; @@ -291,7 +324,7 @@ fn AuraSelection(setup_slot: RwSignal) -> impl IntoView { .collect_view() }; - view! {
{auras}
} + move || view! {
{auras}
} } #[component] @@ -325,6 +358,15 @@ fn AssignmentSelection( }) .collect_view() }; + let unassign = move || { + setup_slot.get().assign_to.map(|_| { + view! { + + } + }) + }; - view! {
{players}
} + move || view! {
{unassign}{players}
} } diff --git a/werewolves/src/app/pages/game/player.rs b/werewolves/src/app/pages/game/player.rs index c6f73ef..530656b 100644 --- a/werewolves/src/app/pages/game/player.rs +++ b/werewolves/src/app/pages/game/player.rs @@ -1,18 +1,13 @@ werewolves_macros::include_path!("werewolves/src/app/pages/game/player"); -use core::{ - hash::Hash, - num::NonZeroU8, - ops::Deref, -}; +use core::{hash::Hash, num::NonZeroU8, ops::Deref}; use std::collections::HashSet; use convert_case::{Case, Casing}; use leptos::prelude::*; use werewolves_proto::{ message::{ - ClientDeadChat, ClientMessage, ServerToClientMessage as Srv2Client, - dead::DeadChatMessage, + ClientDeadChat, ClientMessage, ServerToClientMessage as Srv2Client, dead::DeadChatMessage, }, role::RoleTitle, }; @@ -35,6 +30,7 @@ enum Page { #[component] pub fn PlayerGamePage( + error: WriteSignal>, message: Signal>, reply: WriteSignal>, disconnect: RwSignal, @@ -47,6 +43,13 @@ pub fn PlayerGamePage( return; }; match message { + Srv2Client::GameCancelled => { + error.set(Some("game was cancelled".into())); + gloo::utils::window() + .location() + .replace("/") + .console_log_warn(); + } Srv2Client::Disconnect => disconnect.set(true), Srv2Client::LobbyInfo { joined, diff --git a/werewolves/src/lib.rs b/werewolves/src/lib.rs index 92cb8f4..d4cf670 100644 --- a/werewolves/src/lib.rs +++ b/werewolves/src/lib.rs @@ -39,21 +39,23 @@ pub trait ConsoleLogError { fn console_log_debug(self); } -#[cfg(feature = "hydrate")] -impl ConsoleLogError for Result { +impl ConsoleLogError for Result { fn console_log_warn(self) { + #[cfg(feature = "hydrate")] if let Err(err) = self { gloo::console::warn!(err); } } fn console_log_err(self) { + #[cfg(feature = "hydrate")] if let Err(err) = self { gloo::console::error!(err); } } fn console_log_debug(self) { + #[cfg(feature = "hydrate")] if let Err(err) = self { gloo::console::debug!(err); } diff --git a/werewolves/src/server/lobby.rs b/werewolves/src/server/lobby.rs index 8b781a2..122892f 100644 --- a/werewolves/src/server/lobby.rs +++ b/werewolves/src/server/lobby.rs @@ -64,7 +64,7 @@ impl<'a> Lobby<'a> { } pub const fn settings(&self) -> &GameSettings { - &self.settings + self.settings } pub async fn send_lobby_info_to_clients(&mut self) -> Result<(), ServerError> { @@ -242,7 +242,7 @@ impl<'a> Lobby<'a> { ))) => { self.db .game() - .set_player_number(self.game_id, pid.into(), Some(num)) + .set_player_number(self.game_id, pid, Some(num)) .await?; self.send_lobby_info_to_clients().await.log_debug(loc!()); @@ -257,11 +257,7 @@ impl<'a> Lobby<'a> { }) => { self.db .game() - .join_game( - self.game_id, - identity.player_id.into(), - identity.public.number, - ) + .join_game(self.game_id, identity.player_id, identity.public.number) .await?; self.send_lobby_info_to_clients().await.log_debug(loc!()); @@ -272,10 +268,7 @@ impl<'a> Lobby<'a> { identity: Identification { player_id, .. }, update: ClientUpdate::Message(ClientMessage::Goodbye), }) => { - self.db - .game() - .leave_game(self.game_id, player_id.into()) - .await?; + self.db.game().leave_game(self.game_id, player_id).await?; self.send_lobby_info_to_host().await?; self.send_lobby_info_to_clients().await.log_debug(loc!()); @@ -306,7 +299,7 @@ impl<'a> Lobby<'a> { }) => { self.db .game() - .set_player_number(self.game_id, player_id.into(), Some(number)) + .set_player_number(self.game_id, player_id, Some(number)) .await?; self.send_lobby_info_to_clients().await.log_debug(loc!()); self.send_lobby_info_to_host().await.log_warn(loc!()); diff --git a/werewolves/src/server/mod.rs b/werewolves/src/server/mod.rs index 8af4f13..325cf2e 100644 --- a/werewolves/src/server/mod.rs +++ b/werewolves/src/server/mod.rs @@ -394,4 +394,5 @@ pub async fn delete_game(game: GameId) { for key in player_keys { players.remove(&key); } + log::info!("game {game} is cancelled server-side"); } diff --git a/werewolves/src/server/runner.rs b/werewolves/src/server/runner.rs index 55f6f82..7c43474 100644 --- a/werewolves/src/server/runner.rs +++ b/werewolves/src/server/runner.rs @@ -18,6 +18,8 @@ use core::num::NonZeroU8; use tokio::sync::mpsc::UnboundedReceiver; use werewolves_proto::game::GameId; use werewolves_proto::game_record::{GameRecord, GameRecordState}; +use werewolves_proto::message::ServerToClientMessage; +use werewolves_proto::message::host::ServerToHostMessage; use werewolves_proto::message::{ClientMessage, Identification, host::HostMessage}; use werewolves_proto::{LogError, loc}; @@ -55,8 +57,8 @@ async fn next_message( host_msg = host_recv.recv() => { match host_msg { Some(HostMessage::CancelGame) => { - cancel_game(game_id, db).await; log::info!("got game cancellation request for {game_id}"); + cancel_game(game_id, db).await; None } Some(msg) => Some(HostOrClientMessage::Host(msg)), @@ -70,6 +72,12 @@ async fn next_message( } async fn cancel_game(game_id: GameId, db: &Database) { + static RUNTIME: std::sync::LazyLock = std::sync::LazyLock::new(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("building game destroyer runtime") + }); let game = match db.game().get_game(game_id).await { Ok(g) => g, Err(err) => { @@ -77,11 +85,13 @@ async fn cancel_game(game_id: GameId, db: &Database) { return; } }; + super::send_to_all_players_in_game(game_id, ServerToClientMessage::GameCancelled).await; + super::send_host(game_id, ServerToHostMessage::GameCancelled).await; if let Err(err) = db.game().cancel_game(game.host, game_id).await { log::error!("error cancelling game: {err}"); return; } - tokio::spawn(crate::server::delete_game(game_id)); + RUNTIME.spawn(crate::server::delete_game(game_id)); } async fn add_dummies(game_id: GameId, db: &Database, dummy_count: usize) {