cancellation fixes, aura fixes, etc.

This commit is contained in:
emilis 2026-02-18 01:44:41 +00:00
parent 06294d872e
commit 852973eddf
No known key found for this signature in database
14 changed files with 177 additions and 110 deletions

View File

@ -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;

View File

@ -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<RoleTitle> 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 {

View File

@ -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,7 +803,8 @@ 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
if let ServerToHostMessage::Error(ServerError::GameError(GameError::InvalidTarget)) =
attempt
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::MarkTarget(target.character_id),
)))

View File

@ -66,6 +66,7 @@ pub struct DayCharacter {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
pub enum ServerToClientMessage {
GameCancelled,
Disconnect,
LobbyInfo {
joined: bool,

View File

@ -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]>,

View File

@ -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! {
<Stylesheet id="leptos" href="/pkg/werewolves.css" />
@ -106,6 +107,7 @@ pub fn App() -> impl IntoView {
<Router>
<main>
<Nav />
<ErrorBox msg=error />
<Routes fallback=NotFound>
<Route path=path!("/") view=Main />
<ProtectedRoute
@ -126,7 +128,10 @@ pub fn App() -> impl IntoView {
condition=is_logged_in
redirect_path=|| "/"
/>
<Route path=path!("/games/:id") view=|| view! { <GamePage /> } />
<Route
path=path!("/games/:id")
view=move || view! { <GamePage error=error.write_only() /> }
/>
</Routes>
</main>

View File

@ -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<Option<String>>) -> impl IntoView {
move || {
let params = hooks::use_params_map();
let auth = expect_context::<Store<AuthContext>>();
@ -186,8 +184,9 @@ pub fn GamePage() -> impl IntoView {
view! {
{status}
<HostGamePage message=host_message.into() reply=host_reply.write_only() />
<HostGamePage error=error message=host_message.into() reply=host_reply.write_only() />
<PlayerGamePage
error=error
message=player_message.into()
reply=player_reply.write_only()
disconnect=disconnect

View File

@ -11,20 +11,28 @@ use werewolves_proto::{
},
};
#[cfg(feature = "hydrate")]
use crate::ConsoleLogError;
use crate::app::{Preferences, components::DialogModal};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum HostPage {
#[default]
None,
Settings,
}
#[component]
pub fn HostGamePage(
error: WriteSignal<Option<String>>,
message: Signal<Option<Srv2Host>>,
reply: WriteSignal<Option<HostMessage>>,
) -> impl IntoView {
let prefs = expect_context::<(Signal<Preferences>, WriteSignal<Preferences>)>().0;
let page = RwSignal::new(HostPage::default());
let settings = RwSignal::new(GameSettings::default());
let qr_mode = RwSignal::new(false);
let players: RwSignal<Box<[PlayerState]>> = 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::<HashMap<_, _>>(),
);
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);
}
}
}
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:#?}"),
}
}
});
let cancel = move || {
message
.get()
.is_some()
.then_some(view! { <CancelGame reply=reply prefs=prefs /> })
};
let content = move || match page.get() {
HostPage::None => ().into_any(),
HostPage::Settings => view! {
<Settings
open_categories=open_categories
settings=settings
settings=settings.read_only()
players=players.read_only()
qr_mode=qr_mode
dialog_open=dialog_open
reply=reply
/>
}
.into_any()
}
_ => view! { <h2>{format!("{message:#?}")}</h2> }.into_any(),
}
} else {
().into_any()
}
};
let cancel = move || {
view! {
<CancelGame reply=reply prefs=prefs />
}
.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(
</div>
}
};
view! { {content} }
move || view! { {content} }
}

View File

@ -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<GameSettings>,
settings: ReadSignal<GameSettings>,
players: ReadSignal<Box<[PlayerState]>>,
qr_mode: RwSignal<bool>,
dialog_open: RwSignal<bool>,
dialog_open: RwSignal<HashMap<SlotId, bool>>,
open_categories: RwSignal<HashMap<Category, bool>>,
reply: WriteSignal<Option<HostMessage>>,
) -> 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! { <SettingsSetupSlot setup_slot=signal players=players dialog_open=dialog_open /> }
})
@ -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<SetupSlot>,
players: ReadSignal<Box<[PlayerState]>>,
dialog_open: RwSignal<bool>,
dialog_open: RwSignal<HashMap<SlotId, bool>>,
) -> impl IntoView {
let auras = move || {
let slot = setup_slot.read();
@ -158,18 +165,42 @@ fn SettingsSetupSlot(
}
.into_any()
}
None => {
view! { <span class="missing error">"missing player "{a.to_string()}</span> }
.into_any()
}
None => view! { <span class="missing error">"assigned player not in lobby"</span> }
.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! {
<div class="setup-slot-container">
<DialogModal
open=dialog_open
open=open_signal
mode=DialogMode::Box
button_class=[
"setup-slot",
@ -272,8 +303,10 @@ fn AuraSelection(setup_slot: RwSignal<SetupSlot>) -> 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<SetupSlot>) -> impl IntoView {
.collect_view()
};
view! { <div class="toggle-list">{auras}</div> }
move || view! { <div class="toggle-list">{auras}</div> }
}
#[component]
@ -325,6 +358,15 @@ fn AssignmentSelection(
})
.collect_view()
};
view! { <div class="toggle-list">{players}</div> }
let unassign = move || {
setup_slot.get().assign_to.map(|_| {
view! {
<button on:click=move |_| {
setup_slot.write().assign_to.take();
}>"unassign"</button>
}
})
};
move || view! { <div class="toggle-list">{unassign}{players}</div> }
}

View File

@ -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<Option<String>>,
message: Signal<Option<Srv2Client>>,
reply: WriteSignal<Option<ClientMessage>>,
disconnect: RwSignal<bool>,
@ -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,

View File

@ -39,21 +39,23 @@ pub trait ConsoleLogError {
fn console_log_debug(self);
}
#[cfg(feature = "hydrate")]
impl<T> ConsoleLogError for Result<T, wasm_bindgen::JsValue> {
impl<T> ConsoleLogError for Result<T, leptos::wasm_bindgen::JsValue> {
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);
}

View File

@ -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!());

View File

@ -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");
}

View File

@ -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<tokio::runtime::Runtime> = 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) {