Compare commits

..

No commits in common. "241420757e939b3e65ce1c994d7ba427703dfd9a" and "67d345646d880d5f5b9d4b0b867767f358fb62f4" have entirely different histories.

22 changed files with 76 additions and 961 deletions

12
Cargo.lock generated
View File

@ -191,20 +191,10 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-link", "windows-link",
] ]
[[package]]
name = "chrono-humanize"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b"
dependencies = [
"chrono",
]
[[package]] [[package]]
name = "ciborium" name = "ciborium"
version = "0.2.2" version = "0.2.2"
@ -1970,7 +1960,6 @@ name = "werewolves"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-humanize",
"ciborium", "ciborium",
"convert_case 0.10.0", "convert_case 0.10.0",
"futures", "futures",
@ -2009,7 +1998,6 @@ dependencies = [
name = "werewolves-proto" name = "werewolves-proto"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"colored", "colored",
"log", "log",
"pretty_assertions", "pretty_assertions",

View File

@ -11,7 +11,6 @@ serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.17", features = ["v4", "serde"] } uuid = { version = "1.17", features = ["v4", "serde"] }
rand = { version = "0.9", features = ["std_rng"] } rand = { version = "0.9", features = ["std_rng"] }
werewolves-macros = { path = "../werewolves-macros" } werewolves-macros = { path = "../werewolves-macros" }
chrono = { version = "0.4", features = ["serde"] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = { version = "1" } pretty_assertions = { version = "1" }

View File

@ -107,6 +107,4 @@ pub enum GameError {
MustSelectTarget, MustSelectTarget,
#[error("no current prompt in aura handling")] #[error("no current prompt in aura handling")]
NoCurrentPromptForAura, NoCurrentPromptForAura,
#[error("you're not dead")]
NotDead,
} }

View File

@ -25,10 +25,8 @@ use core::{
ops::{Deref, Range, RangeBounds}, ops::{Deref, Range, RangeBounds},
}; };
use chrono::{DateTime, Utc};
use rand::{Rng, seq::SliceRandom}; use rand::{Rng, seq::SliceRandom};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{ use crate::{
character::CharacterId, character::CharacterId,
@ -38,12 +36,10 @@ use crate::{
story::{DayDetail, GameActions, GameStory, NightDetails}, story::{DayDetail, GameActions, GameStory, NightDetails},
}, },
message::{ message::{
CharacterState, ClientDeadChat, Identification, ServerToClientMessage, CharacterState, Identification,
dead::{DeadChatContent, DeadChatMessage},
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::ActionResponse, night::ActionResponse,
}, },
player::PlayerId,
}; };
pub use { pub use {
@ -55,7 +51,6 @@ type Result<T> = core::result::Result<T, GameError>;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Game { pub struct Game {
started: DateTime<Utc>,
history: GameStory, history: GameStory,
state: GameState, state: GameState,
} }
@ -64,7 +59,6 @@ impl Game {
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> { pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
let village = Village::new(players, settings)?; let village = Village::new(players, settings)?;
Ok(Self { Ok(Self {
started: Utc::now(),
history: GameStory::new(village.clone()), history: GameStory::new(village.clone()),
state: GameState::Night { state: GameState::Night {
night: Night::new(village)?, night: Night::new(village)?,
@ -72,10 +66,6 @@ impl Game {
}) })
} }
pub const fn started(&self) -> DateTime<Utc> {
self.started
}
pub const fn village(&self) -> &Village { pub const fn village(&self) -> &Village {
match &self.state { match &self.state {
GameState::Day { village, marked: _ } => village, GameState::Day { village, marked: _ } => village,
@ -83,50 +73,6 @@ impl Game {
} }
} }
pub fn process_dead_chat_request(
&mut self,
player_id: PlayerId,
message: ClientDeadChat,
) -> Result<ServerToClientMessage> {
let char = self
.village()
.character_by_player_id(player_id)
.ok_or(GameError::NoMatchingCharacterFound)?;
match message {
ClientDeadChat::Send(message) => {
let msg = DeadChatMessage {
id: Uuid::new_v4(),
timestamp: Utc::now(),
message: DeadChatContent::PlayerMessage {
message,
from: char.identity(),
},
};
match &mut self.state {
GameState::Day { village, .. } => {
village.send_dead_chat_message(msg.clone())?
}
GameState::Night { night } => night.send_dead_chat_message(msg.clone())?,
}
Ok(ServerToClientMessage::DeadChatMessage(msg))
}
ClientDeadChat::GetHistory => {
let messages = self
.village()
.dead_chat()
.get_since(self.started, char.character_id());
Ok(ServerToClientMessage::DeadChat(messages))
}
ClientDeadChat::GetSince(since) => {
let messages = self
.village()
.dead_chat()
.get_since(since, char.character_id());
Ok(ServerToClientMessage::DeadChat(messages))
}
}
}
#[cfg(test)] #[cfg(test)]
#[doc(hidden)] #[doc(hidden)]
pub const fn village_mut(&mut self) -> &mut Village { pub const fn village_mut(&mut self) -> &mut Village {
@ -199,7 +145,6 @@ impl Game {
.collect(), .collect(),
), ),
)?; )?;
self.state = GameState::Night { night }; self.state = GameState::Night { night };
self.process(HostGameMessage::GetState) self.process(HostGameMessage::GetState)
} }
@ -458,7 +403,7 @@ impl Ord for GameTime {
} }
} }
(GameTime::Night { number: l }, GameTime::Day { number: r }) => { (GameTime::Night { number: l }, GameTime::Day { number: r }) => {
if *l >= r.get() { if *l > r.get() {
Ordering::Greater Ordering::Greater
} else { } else {
Ordering::Less Ordering::Less

View File

@ -32,11 +32,8 @@ use crate::{
kill::{self, KillOutcome}, kill::{self, KillOutcome},
night::changes::{ChangesLookup, NightChange}, night::changes::{ChangesLookup, NightChange},
}, },
message::{ message::night::{
dead::DeadChatMessage, ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, ActionType, Visits,
night::{
ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, ActionType, Visits,
},
}, },
role::RoleTitle, role::RoleTitle,
}; };
@ -1224,10 +1221,6 @@ impl Night {
&self.village &self.village
} }
pub fn send_dead_chat_message(&mut self, msg: DeadChatMessage) -> Result<()> {
self.village.send_dead_chat_message(msg)
}
#[cfg(test)] #[cfg(test)]
#[doc(hidden)] #[doc(hidden)]
pub const fn village_mut(&mut self) -> &mut Village { pub const fn village_mut(&mut self) -> &mut Village {

View File

@ -24,11 +24,7 @@ use crate::{
diedto::DiedTo, diedto::DiedTo,
error::GameError, error::GameError,
game::{GameOver, GameSettings, GameTime}, game::{GameOver, GameSettings, GameTime},
message::{ message::{CharacterIdentity, Identification, night::ActionPrompt},
CharacterIdentity, Identification,
dead::{DeadChat, DeadChatMessage},
night::ActionPrompt,
},
player::PlayerId, player::PlayerId,
role::{Role, RoleTitle}, role::{Role, RoleTitle},
}; };
@ -37,7 +33,6 @@ use crate::{
pub struct Village { pub struct Village {
characters: Box<[Character]>, characters: Box<[Character]>,
time: GameTime, time: GameTime,
dead_chat: DeadChat,
settings: GameSettings, settings: GameSettings,
} }
@ -57,18 +52,9 @@ impl Village {
settings, settings,
characters, characters,
time: GameTime::Night { number: 0 }, time: GameTime::Night { number: 0 },
dead_chat: DeadChat::new(),
}) })
} }
pub const fn dead_chat(&self) -> &DeadChat {
&self.dead_chat
}
pub fn send_dead_chat_message(&mut self, msg: DeadChatMessage) -> Result<()> {
self.dead_chat.add(self.time, msg)
}
pub fn settings(&self) -> GameSettings { pub fn settings(&self) -> GameSettings {
self.settings.clone() self.settings.clone()
} }
@ -184,25 +170,14 @@ impl Village {
return Ok(Some(game_over)); return Ok(Some(game_over));
} }
self.time = self.time.next(); self.time = self.time.next();
self.dead_chat_time_transition();
Ok(None) Ok(None)
} }
fn dead_chat_time_transition(&mut self) {
self.dead_chat.set_dead(
self.characters
.iter()
.filter_map(|c| c.died_to().map(|died_to| (died_to.date_time(), c.clone()))),
self.time,
)
}
pub fn to_day(&mut self) -> Result<GameTime> { pub fn to_day(&mut self) -> Result<GameTime> {
if self.time.is_day() { if self.time.is_day() {
return Err(GameError::AlreadyDaytime); return Err(GameError::AlreadyDaytime);
} }
self.time = self.time.next(); self.time = self.time.next();
self.dead_chat_time_transition();
Ok(self.time) Ok(self.time)
} }

View File

@ -19,7 +19,6 @@ mod previous;
mod revert; mod revert;
mod role; mod role;
mod skip; mod skip;
mod time;
use crate::{ use crate::{
character::{Character, CharacterId}, character::{Character, CharacterId},

View File

@ -1,48 +0,0 @@
// Copyright (C) 2026 Emilis Bliūdžius
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use core::{cmp::Ordering, num::NonZeroU8};
#[allow(unused)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use crate::game::GameTime;
#[test]
pub fn game_time_test() {
for (l, r, exp) in &[
(
GameTime::Day {
number: NonZeroU8::new(1).unwrap(),
},
GameTime::Night { number: 0 },
Ordering::Greater,
),
(
GameTime::Night { number: 1 },
GameTime::Day {
number: NonZeroU8::new(1).unwrap(),
},
Ordering::Greater,
),
(
GameTime::Night { number: 0 },
GameTime::Day {
number: NonZeroU8::new(1).unwrap(),
},
Ordering::Less,
),
] {
assert_eq!(l.cmp(r), *exp);
}
}

View File

@ -12,22 +12,18 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod dead;
pub mod host; pub mod host;
mod ident; mod ident;
pub mod night; pub mod night;
use core::num::NonZeroU8; use core::num::NonZeroU8;
use chrono::{DateTime, Utc};
pub use ident::*; pub use ident::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
character::CharacterId, character::CharacterId,
error::GameError,
game::{GameOver, story::GameStory}, game::{GameOver, story::GameStory},
message::dead::DeadChatMessage,
role::RoleTitle, role::RoleTitle,
}; };
@ -38,14 +34,6 @@ pub enum ClientMessage {
GetState, GetState,
RoleAck, RoleAck,
UpdateSelf(UpdateSelf), UpdateSelf(UpdateSelf),
DeadChat(ClientDeadChat),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ClientDeadChat {
Send(String),
GetHistory,
GetSince(DateTime<Utc>),
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -63,7 +51,7 @@ pub struct DayCharacter {
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerToClientMessage { pub enum ServerMessage {
Disconnect, Disconnect,
LobbyInfo { LobbyInfo {
joined: bool, joined: bool,
@ -78,11 +66,8 @@ pub enum ServerToClientMessage {
GameOver(GameOver), GameOver(GameOver),
Story(GameStory), Story(GameStory),
Update(PlayerUpdate), Update(PlayerUpdate),
DeadChat(Box<[DeadChatMessage]>),
DeadChatMessage(DeadChatMessage),
Sleep, Sleep,
Reset, Reset,
Error(GameError),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -1,193 +0,0 @@
// Copyright (C) 2025-2026 Emilis Bliūdžius
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
character::{Character, CharacterId},
diedto::DiedTo,
error::GameError,
game::GameTime,
message::CharacterIdentity,
};
type Result<T> = core::result::Result<T, GameError>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeadChat {
deaths: Vec<(GameTime, CharacterId)>,
messages: HashMap<GameTime, Vec<DeadChatMessage>>,
}
impl DeadChat {
pub fn new() -> Self {
let mut messages = HashMap::new();
messages.insert(
GameTime::Night { number: 0 },
vec![DeadChatMessage {
id: Uuid::new_v4(),
timestamp: Utc::now(),
message: DeadChatContent::TimeChange(GameTime::Night { number: 0 }),
}],
);
Self {
messages,
deaths: Vec::new(),
}
}
pub fn all_character_ids(&self) -> Box<[CharacterId]> {
self.deaths.iter().map(|c| c.1).collect()
}
pub fn sort(&mut self) {
self.messages
.iter_mut()
.for_each(|(_, v)| v.sort_by_key(|s| s.timestamp));
}
pub fn add(&mut self, time: GameTime, msg: DeadChatMessage) -> Result<()> {
if !self
.deaths
.iter()
.any(|(t, ch)| time >= *t && msg.message.is_from_character(*ch))
{
return Err(GameError::NotDead);
}
if let Some(msgs) = self.messages.get_mut(&time) {
msgs.push(msg);
} else {
self.messages.insert(time, vec![msg]);
}
Ok(())
}
pub fn set_dead(
&mut self,
dead: impl Iterator<Item = (GameTime, Character)> + Clone,
time: GameTime,
) {
let newly_dead = dead
.clone()
.filter_map(|(t, c)| (t.next() == time).then_some(c))
.collect::<Box<_>>();
self.deaths = dead.clone().map(|(t, c)| (t, c.character_id())).collect();
let Some(prev) = time.previous() else {
return;
};
let messages = if let Some(messages) = self.messages.get_mut(&prev) {
messages
} else {
self.messages.insert(
prev,
vec![DeadChatMessage {
id: Uuid::new_v4(),
timestamp: Utc::now(),
message: DeadChatContent::TimeChange(prev),
}],
);
self.messages.get_mut(&prev).unwrap()
};
for dead in newly_dead {
let Some(died_to) = dead.died_to() else {
continue;
};
messages.push(DeadChatMessage {
id: Uuid::new_v4(),
timestamp: Utc::now(),
message: DeadChatContent::Death {
character: dead.identity(),
cause: died_to.clone(),
},
});
}
messages.sort_by_key(|c| c.timestamp);
let id = Uuid::new_v4();
if let Some(existing) = self.messages.insert(
time,
vec![DeadChatMessage {
id,
timestamp: Utc::now(),
message: DeadChatContent::TimeChange(time),
}],
) {
log::warn!("replaced: {existing:?}");
}
}
pub fn get_since(&self, t: DateTime<Utc>, character: CharacterId) -> Box<[DeadChatMessage]> {
let times_applicable = self
.deaths
.iter()
.filter_map(|(dt, c)| (*c == character).then_some(*dt))
.collect::<Box<[_]>>();
let mut messages = self
.messages
.iter()
.filter_map(|(gt, c)| {
times_applicable
.iter()
.any(|t| t <= gt)
.then_some(c.iter().filter(|c| c.timestamp >= t))
})
.flatten()
.cloned()
.collect::<Box<_>>();
messages.sort_by_key(|m| m.timestamp);
messages
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeadChatMessage {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub message: DeadChatContent,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DeadChatContent {
PlayerMessage {
from: CharacterIdentity,
message: String,
},
Death {
character: CharacterIdentity,
cause: DiedTo,
},
TimeChange(GameTime),
}
impl DeadChatContent {
pub fn is_from_character(&self, character_id: CharacterId) -> bool {
match self {
DeadChatContent::PlayerMessage { from, .. } => from.character_id == character_id,
DeadChatContent::Death { character, .. } => character.character_id == character_id,
DeadChatContent::TimeChange(_) => false,
}
}
pub const fn message(&self) -> Option<&str> {
match self {
DeadChatContent::PlayerMessage { message, .. } => Some(message.as_str()),
_ => None,
}
}
}

View File

@ -30,7 +30,7 @@ use axum_extra::TypedHeader;
use chrono::Utc; use chrono::Utc;
use colored::Colorize; use colored::Colorize;
use tokio::sync::broadcast::{Receiver, Sender}; use tokio::sync::broadcast::{Receiver, Sender};
use werewolves_proto::message::{ClientMessage, Identification, ServerToClientMessage, UpdateSelf}; use werewolves_proto::message::{ClientMessage, Identification, ServerMessage, UpdateSelf};
pub async fn handler( pub async fn handler(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
@ -142,7 +142,7 @@ struct Client {
socket: WebSocket, socket: WebSocket,
who: String, who: String,
sender: Sender<IdentifiedClientMessage>, sender: Sender<IdentifiedClientMessage>,
receiver: Receiver<ServerToClientMessage>, receiver: Receiver<ServerMessage>,
} }
impl Client { impl Client {
@ -152,7 +152,7 @@ impl Client {
socket: WebSocket, socket: WebSocket,
who: String, who: String,
sender: Sender<IdentifiedClientMessage>, sender: Sender<IdentifiedClientMessage>,
receiver: Receiver<ServerToClientMessage>, receiver: Receiver<ServerMessage>,
) -> Self { ) -> Self {
Self { Self {
ident, ident,
@ -240,7 +240,7 @@ impl Client {
Ok(()) Ok(())
} }
async fn handle_message(&mut self, message: ServerToClientMessage) -> Result<(), anyhow::Error> { async fn handle_message(&mut self, message: ServerMessage) -> Result<(), anyhow::Error> {
self.socket self.socket
.send({ .send({
#[cfg(not(feature = "cbor"))] #[cfg(not(feature = "cbor"))]

View File

@ -23,7 +23,7 @@ use tokio::{
time::Instant, time::Instant,
}; };
use werewolves_proto::{ use werewolves_proto::{
message::{PublicIdentity, ServerToClientMessage}, message::{PublicIdentity, ServerMessage},
player::PlayerId, player::PlayerId,
}; };
@ -44,8 +44,8 @@ impl ConnectionId {
#[derive(Debug)] #[derive(Debug)]
pub struct JoinedPlayer { pub struct JoinedPlayer {
sender: Sender<ServerToClientMessage>, sender: Sender<ServerMessage>,
receiver: Receiver<ServerToClientMessage>, receiver: Receiver<ServerMessage>,
active_connection: ConnectionId, active_connection: ConnectionId,
in_game: bool, in_game: bool,
pub name: String, pub name: String,
@ -55,8 +55,8 @@ pub struct JoinedPlayer {
impl JoinedPlayer { impl JoinedPlayer {
pub const fn new( pub const fn new(
sender: Sender<ServerToClientMessage>, sender: Sender<ServerMessage>,
receiver: Receiver<ServerToClientMessage>, receiver: Receiver<ServerMessage>,
active_connection: ConnectionId, active_connection: ConnectionId,
name: String, name: String,
number: Option<NonZeroU8>, number: Option<NonZeroU8>,
@ -72,7 +72,7 @@ impl JoinedPlayer {
in_game: false, in_game: false,
} }
} }
pub fn resubscribe_reciever(&self) -> Receiver<ServerToClientMessage> { pub fn resubscribe_reciever(&self) -> Receiver<ServerMessage> {
self.receiver.resubscribe() self.receiver.resubscribe()
} }
} }
@ -91,7 +91,7 @@ impl JoinedPlayers {
pub async fn send_to_all_filter( pub async fn send_to_all_filter(
&self, &self,
message: ServerToClientMessage, message: ServerMessage,
filter: impl Fn(PlayerId) -> bool, filter: impl Fn(PlayerId) -> bool,
) { ) {
let players: tokio::sync::MutexGuard<'_, HashMap<PlayerId, JoinedPlayer>> = let players: tokio::sync::MutexGuard<'_, HashMap<PlayerId, JoinedPlayer>> =
@ -107,7 +107,7 @@ impl JoinedPlayers {
} }
} }
pub async fn send_to(&self, player_ids: &[PlayerId], message: ServerToClientMessage) { pub async fn send_to(&self, player_ids: &[PlayerId], message: ServerMessage) {
let players: tokio::sync::MutexGuard<'_, HashMap<PlayerId, JoinedPlayer>> = let players: tokio::sync::MutexGuard<'_, HashMap<PlayerId, JoinedPlayer>> =
self.players.lock().await; self.players.lock().await;
let senders = players let senders = players
@ -130,7 +130,7 @@ impl JoinedPlayers {
.collect::<Box<[_]>>(); .collect::<Box<[_]>>();
core::mem::drop(players); core::mem::drop(players);
for (pid, send) in senders { for (pid, send) in senders {
send.send(ServerToClientMessage::LobbyInfo { send.send(ServerMessage::LobbyInfo {
joined: in_lobby_ids.contains(&pid), joined: in_lobby_ids.contains(&pid),
players: in_lobby.clone(), players: in_lobby.clone(),
}) })
@ -181,7 +181,7 @@ impl JoinedPlayers {
None None
} }
pub async fn get_sender(&self, player_id: PlayerId) -> Option<Sender<ServerToClientMessage>> { pub async fn get_sender(&self, player_id: PlayerId) -> Option<Sender<ServerMessage>> {
self.players self.players
.lock() .lock()
.await .await
@ -193,7 +193,7 @@ impl JoinedPlayers {
&self, &self,
player_id: PlayerId, player_id: PlayerId,
player: JoinedPlayer, player: JoinedPlayer,
) -> Receiver<ServerToClientMessage> { ) -> Receiver<ServerMessage> {
let mut map = self.players.lock().await; let mut map = self.players.lock().await;
if let Some(old) = map.insert(player_id, player) { if let Some(old) = map.insert(player_id, player) {

View File

@ -27,8 +27,7 @@ use werewolves_proto::{
error::GameError, error::GameError,
game::{Game, GameOver, Village}, game::{Game, GameOver, Village},
message::{ message::{
ClientDeadChat, ClientMessage, Identification, ServerToClientMessage, ClientMessage, Identification, ServerMessage,
dead::DeadChatMessage,
host::{HostGameMessage, HostMessage, PostGameMessage, ServerToHostMessage}, host::{HostGameMessage, HostMessage, PostGameMessage, ServerToHostMessage},
}, },
player::PlayerId, player::PlayerId,
@ -80,7 +79,7 @@ impl GameRunner {
for char in characters.iter() { for char in characters.iter() {
match self.player_sender.send_if_present( match self.player_sender.send_if_present(
char.player_id(), char.player_id(),
ServerToClientMessage::GameStart { ServerMessage::GameStart {
role: char.initial_shown_role(), role: char.initial_shown_role(),
}, },
) { ) {
@ -98,7 +97,7 @@ impl GameRunner {
} }
} }
self.joined_players self.joined_players
.send_to_all_filter(ServerToClientMessage::GameInProgress, |pid| { .send_to_all_filter(ServerMessage::GameInProgress, |pid| {
!characters.iter().any(|c| c.player_id() == pid) !characters.iter().any(|c| c.player_id() == pid)
}) })
.await; .await;
@ -132,7 +131,7 @@ impl GameRunner {
&& let Some(sender) = sender.get_sender(player_id).await && let Some(sender) = sender.get_sender(player_id).await
{ {
sender sender
.send(ServerToClientMessage::GameStart { .send(ServerMessage::GameStart {
role: char.initial_shown_role(), role: char.initial_shown_role(),
}) })
.log_debug(); .log_debug();
@ -198,7 +197,7 @@ impl GameRunner {
}; };
if acks.iter().any(|(c, d)| c.player_id() == player_id && *d) { if acks.iter().any(|(c, d)| c.player_id() == player_id && *d) {
// already ack'd just sleep // already ack'd just sleep
sender.send(ServerToClientMessage::Sleep).log_debug(); sender.send(ServerMessage::Sleep).log_debug();
continue; continue;
} }
if let Some(char) = self if let Some(char) = self
@ -209,15 +208,13 @@ impl GameRunner {
.find(|c| c.player_id() == player_id) .find(|c| c.player_id() == player_id)
{ {
sender sender
.send(ServerToClientMessage::GameStart { .send(ServerMessage::GameStart {
role: char.initial_shown_role(), role: char.initial_shown_role(),
}) })
.log_debug(); .log_debug();
} else { } else {
log::info!("game in progress for {player_id}"); log::info!("game in progress for {player_id}");
sender sender.send(ServerMessage::GameInProgress).log_debug();
.send(ServerToClientMessage::GameInProgress)
.log_debug();
} }
log::info!("player {player_id} end"); log::info!("player {player_id} end");
} }
@ -234,12 +231,12 @@ impl GameRunner {
{ {
*ackd = true; *ackd = true;
self.player_sender self.player_sender
.send_if_present(player_id, ServerToClientMessage::Sleep) .send_if_present(player_id, ServerMessage::Sleep)
.log_debug(); .log_debug();
} }
(update_host)(&acks, &mut self.comms); (update_host)(&acks, &mut self.comms);
if let Some(sender) = self.joined_players.get_sender(player_id).await { if let Some(sender) = self.joined_players.get_sender(player_id).await {
sender.send(ServerToClientMessage::Sleep).log_debug(); sender.send(ServerMessage::Sleep).log_debug();
} }
} }
Message::Client(IdentifiedClientMessage { Message::Client(IdentifiedClientMessage {
@ -251,7 +248,7 @@ impl GameRunner {
for char in self.game.village().characters() { for char in self.game.village().characters() {
if let Some(sender) = self.joined_players.get_sender(char.player_id()).await { if let Some(sender) = self.joined_players.get_sender(char.player_id()).await {
let _ = sender.send(ServerToClientMessage::Sleep); let _ = sender.send(ServerMessage::Sleep);
} }
} }
@ -264,74 +261,13 @@ impl GameRunner {
update: ClientUpdate::ConnectStateUpdate, update: ClientUpdate::ConnectStateUpdate,
.. ..
})) => return None, })) => return None,
Ok(Message::Client(IdentifiedClientMessage {
identity: Identification { player_id, .. },
update: ClientUpdate::Message(ClientMessage::DeadChat(chat_request)),
})) => {
if let ClientDeadChat::Send(msg) = &chat_request
&& msg.trim().is_empty()
{
return None;
}
let reply = match self.game.process_dead_chat_request(player_id, chat_request) {
Ok(msg) => msg,
Err(err) => ServerToClientMessage::Error(err),
};
match reply {
ServerToClientMessage::DeadChatMessage(msg) => {
if let Err(err) = self.send_dead_message(&msg).await {
log::warn!("sending message {msg:?} to dead chat: {err}");
}
}
other => {
if let Some(sender) = self.joined_players.get_sender(player_id).await
&& let Err(err) = sender.send(other.clone())
{
log::warn!("sending message {other:?} to [{player_id}]: {err}");
}
}
}
return None;
}
Ok(Message::Client(IdentifiedClientMessage {
identity: Identification { player_id, .. },
update: ClientUpdate::Message(ClientMessage::GetState),
})) => {
let Some(char) = self.game.village().character_by_player_id(player_id) else {
if let Some(send) = self.joined_players.get_sender(player_id).await {
send.send(ServerToClientMessage::GameInProgress).log_debug();
}
return None;
};
if !self
.game
.village()
.dead_chat()
.all_character_ids()
.contains(&char.character_id())
{
if let Some(send) = self.joined_players.get_sender(player_id).await {
send.send(ServerToClientMessage::GameInProgress).log_debug();
}
return None;
}
let msg = match self
.game
.process_dead_chat_request(player_id, ClientDeadChat::GetHistory)
{
Ok(msg) => msg,
Err(err) => ServerToClientMessage::Error(err),
};
self.joined_players.send_to(&[player_id], msg).await;
return None;
}
Ok(Message::Client(IdentifiedClientMessage { Ok(Message::Client(IdentifiedClientMessage {
identity: Identification { player_id, .. }, identity: Identification { player_id, .. },
.. ..
})) => { })) => {
log::info!("client message from player {player_id}"); log::info!("client message from player {player_id}");
if let Some(send) = self.joined_players.get_sender(player_id).await { if let Some(send) = self.joined_players.get_sender(player_id).await {
send.send(ServerToClientMessage::GameInProgress).log_debug(); send.send(ServerMessage::GameInProgress).log_debug();
} }
return None; return None;
} }
@ -342,7 +278,6 @@ impl GameRunner {
} }
}; };
let pre_time = self.game.village().time();
match self.host_message(msg) { match self.host_message(msg) {
Ok(resp) => { Ok(resp) => {
self.comms.host().send(resp).log_warn(); self.comms.host().send(resp).log_warn();
@ -354,53 +289,7 @@ impl GameRunner {
.log_warn(); .log_warn();
} }
} }
let post_time = self.game.village().time(); self.game.game_over()
if let Some(game_over) = self.game.game_over() {
return Some(game_over);
}
if pre_time != post_time {
let newly_dead = self
.game
.village()
.dead_characters()
.into_iter()
.filter_map(|c| c.died_to().map(|_| (c.character_id(), c.player_id())));
for (char, player) in newly_dead {
let msgs = self
.game
.village()
.dead_chat()
.get_since(self.game.started(), char);
self.joined_players
.send_to(&[player], ServerToClientMessage::DeadChat(msgs))
.await;
}
}
None
}
pub async fn send_dead_message(&mut self, msg: &DeadChatMessage) -> Result<()> {
let player_ids = self
.game
.village()
.dead_chat()
.all_character_ids()
.into_iter()
.map(|c| {
self.game
.village()
.character_by_id(c)
.map(|c| c.player_id())
})
.collect::<Result<Box<_>>>()?;
self.joined_players
.send_to(
&player_ids,
ServerToClientMessage::DeadChatMessage(msg.clone()),
)
.await;
Ok(())
} }
pub fn host_message(&mut self, message: HostMessage) -> Result<ServerToHostMessage> { pub fn host_message(&mut self, message: HostMessage) -> Result<ServerToHostMessage> {
@ -421,7 +310,7 @@ impl GameRunner {
enum ProcessOutcome { enum ProcessOutcome {
Lobby(Lobby), Lobby(Lobby),
SendPlayer(PlayerId, ServerToClientMessage), SendPlayer(PlayerId, ServerMessage),
} }
pub struct GameEnd { pub struct GameEnd {
@ -462,7 +351,7 @@ impl GameEnd {
.collect::<Box<[_]>>(); .collect::<Box<[_]>>();
game.joined_players game.joined_players
.send_to(&player_ids, ServerToClientMessage::Story(story)) .send_to(&player_ids, ServerMessage::Story(story))
.await; .await;
} }
let msg = match self.game().unwrap().comms.message().await { let msg = match self.game().unwrap().comms.message().await {
@ -563,7 +452,7 @@ impl GameEnd {
let story = self.game().ok()?.game.story(); let story = self.game().ok()?.game.story();
return Some(ProcessOutcome::SendPlayer( return Some(ProcessOutcome::SendPlayer(
identity.player_id, identity.player_id,
ServerToClientMessage::Story(story), ServerMessage::Story(story),
)); ));
} }
} }

View File

@ -22,7 +22,7 @@ use werewolves_proto::{
error::GameError, error::GameError,
game::{Game, GameSettings}, game::{Game, GameSettings},
message::{ message::{
ClientMessage, Identification, PlayerState, PublicIdentity, ServerToClientMessage, ClientMessage, Identification, PlayerState, PublicIdentity, ServerMessage,
host::{HostLobbyMessage, HostMessage, ServerToHostMessage}, host::{HostLobbyMessage, HostMessage, ServerToHostMessage},
}, },
player::PlayerId, player::PlayerId,
@ -149,7 +149,7 @@ impl Lobby {
)) => { )) => {
let _ = self let _ = self
.players_in_lobby .players_in_lobby
.send_if_present(player_id, ServerToClientMessage::InvalidMessageForGameState); .send_if_present(player_id, ServerMessage::InvalidMessageForGameState);
} }
Err(( Err((
Message::Client(IdentifiedClientMessage { Message::Client(IdentifiedClientMessage {
@ -168,7 +168,7 @@ impl Lobby {
log::error!("processing message from {public} [{player_id}]: {err}"); log::error!("processing message from {public} [{player_id}]: {err}");
let _ = self let _ = self
.players_in_lobby .players_in_lobby
.send_if_present(player_id, ServerToClientMessage::Reset); .send_if_present(player_id, ServerMessage::Reset);
} }
} }
None None
@ -176,12 +176,6 @@ impl Lobby {
async fn next_inner(&mut self, msg: Message) -> Result<Option<GameRunner>, GameError> { async fn next_inner(&mut self, msg: Message) -> Result<Option<GameRunner>, GameError> {
match msg { match msg {
Message::Client(IdentifiedClientMessage {
update: ClientUpdate::Message(ClientMessage::DeadChat(_)),
..
}) => {
log::warn!("dead chat message in lobby? ignoring.");
}
Message::Host(HostMessage::Lobby(HostLobbyMessage::ManufacturePlayer(public))) => { Message::Host(HostMessage::Lobby(HostLobbyMessage::ManufacturePlayer(public))) => {
log::info!("adding player {public:?} by host request"); log::info!("adding player {public:?} by host request");
self.players_in_lobby.push(( self.players_in_lobby.push((
@ -290,7 +284,7 @@ impl Lobby {
identity: Identification { player_id, .. }, identity: Identification { player_id, .. },
update: ClientUpdate::Message(ClientMessage::GetState), update: ClientUpdate::Message(ClientMessage::GetState),
}) => { }) => {
let msg = ServerToClientMessage::LobbyInfo { let msg = ServerMessage::LobbyInfo {
joined: self joined: self
.players_in_lobby .players_in_lobby
.iter() .iter()
@ -344,10 +338,10 @@ impl Lobby {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct LobbyPlayers(Vec<(Identification, Option<Sender<ServerToClientMessage>>)>); pub struct LobbyPlayers(Vec<(Identification, Option<Sender<ServerMessage>>)>);
impl Deref for LobbyPlayers { impl Deref for LobbyPlayers {
type Target = Vec<(Identification, Option<Sender<ServerToClientMessage>>)>; type Target = Vec<(Identification, Option<Sender<ServerMessage>>)>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
@ -380,7 +374,7 @@ impl LobbyPlayers {
.collect(), .collect(),
) )
} }
pub fn find(&self, player_id: PlayerId) -> Option<&Sender<ServerToClientMessage>> { pub fn find(&self, player_id: PlayerId) -> Option<&Sender<ServerMessage>> {
self.iter() self.iter()
.filter_map(|(id, s)| s.as_ref().map(|s| (id, s))) .filter_map(|(id, s)| s.as_ref().map(|s| (id, s)))
.find_map(|(id, s)| (id.player_id == player_id).then_some(s)) .find_map(|(id, s)| (id.player_id == player_id).then_some(s))
@ -389,7 +383,7 @@ impl LobbyPlayers {
pub fn send_if_present( pub fn send_if_present(
&self, &self,
player_id: PlayerId, player_id: PlayerId,
message: ServerToClientMessage, message: ServerMessage,
) -> Result<bool, GameError> { ) -> Result<bool, GameError> {
if let Some(sender) = self.find(player_id) { if let Some(sender) = self.find(player_id) {
sender sender

View File

@ -37,7 +37,6 @@ wasm-bindgen-futures = "0.4"
thiserror = { version = "2" } thiserror = { version = "2" }
convert_case = { version = "0.10" } convert_case = { version = "0.10" }
ciborium = { version = "0.2", optional = true } ciborium = { version = "0.2", optional = true }
chrono-humanize = { version = "0.2.3", features = ["wasmbind"] }
[features] [features]
default = ["cbor"] default = ["cbor"]

View File

@ -2931,99 +2931,3 @@ dialog {
color: $damned_color; color: $damned_color;
} }
} }
.dead-chat {
height: 100%;
// width: 100%;
// max-width: 100vw;
padding: 0px 3% 0 3%;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
flex-grow: 1;
overflow-x: hidden;
gap: 3px;
.chat-messages {
user-select: text;
padding-inline-start: 0px;
overflow-y: scroll;
display: flex;
flex-direction: column;
margin: 0;
flex-grow: 1;
max-width: 100%;
max-height: 95vh;
justify-content: flex-end;
// scrollbar-width: thin;
scrollbar-width: none;
scrollbar-color: rgba(255, 255, 255, 0.7) black;
}
.message {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-left: 0;
gap: 1ch;
align-items: baseline;
.dead-ident {
font-weight: bold;
&[pronouns]:hover::after {
content: attr(pronouns);
overflow-y: hidden;
position: relative;
color: white;
background-color: black;
border: 1px solid white;
padding: 3px;
z-index: 4;
align-self: center;
justify-self: center;
}
}
.time {
flex-shrink: 1;
opacity: 50%;
font-size: 1em;
}
.message-content {
flex-grow: 1;
font-size: 1.2em;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: baseline;
gap: 1ch;
}
.time-change {
width: 100%;
text-align: center;
}
}
form {
max-width: 100%;
margin: 0;
flex-grow: 1;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
padding-bottom: 10px;
max-height: 5ch;
}
#message-input {
padding: 5px 0px 5px 0px;
padding-inline: 0px 3px 0px 3px;
align-self: flex-end;
width: 100%;
flex-grow: 1;
}
}

View File

@ -12,10 +12,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use core::{ use core::sync::atomic::{AtomicBool, AtomicI64, Ordering};
ops::Not,
sync::atomic::{AtomicBool, AtomicI64, Ordering},
};
use std::rc::Rc; use std::rc::Rc;
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
@ -23,9 +20,7 @@ use gloo::storage::errors::StorageError;
use wasm_bindgen::{JsCast, prelude::Closure}; use wasm_bindgen::{JsCast, prelude::Closure};
use werewolves_proto::{ use werewolves_proto::{
game::story::GameStory, game::story::GameStory,
message::{ message::{ClientMessage, Identification, PublicIdentity},
ClientDeadChat, ClientMessage, Identification, PublicIdentity, dead::DeadChatMessage,
},
player::PlayerId, player::PlayerId,
role::RoleTitle, role::RoleTitle,
}; };
@ -56,9 +51,6 @@ pub enum ClientEvent2 {
}, },
Story(GameStory), Story(GameStory),
GameInProgress, GameInProgress,
DeadChat {
messages: Vec<DeadChatMessage>,
},
} }
#[derive(Default, Clone, PartialEq)] #[derive(Default, Clone, PartialEq)]
@ -82,11 +74,7 @@ pub(super) fn time_spent_unfocused() -> Option<TimeDelta> {
#[function_component] #[function_component]
pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html { pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
let ident_state = use_state(|| { let ident_state = use_state(|| Option::<(PlayerId, PublicIdentity)>::None);
PlayerId::load_from_storage()
.and_then(|pid| PublicIdentity::load_from_storage().map(|ident| (pid, ident)))
.ok()
});
if gloo::utils::window().onfocus().is_none() { if gloo::utils::window().onfocus().is_none() {
let on_focus = { let on_focus = {
@ -166,24 +154,6 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
}; };
let content = match &*client_state { let content = match &*client_state {
ClientEvent2::DeadChat { messages } => {
let on_send = {
let send = send.clone();
move |msg: String| {
if let Err(err) =
send.send_now(ClientMessage::DeadChat(ClientDeadChat::Send(msg)))
{
log::error!("sending dead chat message: {err}");
}
}
};
html! {
<crate::components::chat::DeadChat
on_send={on_send}
messages={messages.clone()}
/>
}
}
ClientEvent2::Signin => html! { ClientEvent2::Signin => html! {
<Signin callback={on_signin} /> <Signin callback={on_signin} />
}, },
@ -290,9 +260,8 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
} }
} }
}; };
let dead_chat = matches!(&*client_state, ClientEvent2::DeadChat { .. });
let nav = dead_chat.not().then_some({ let nav = {
let send = (*send).clone(); let send = (*send).clone();
let error_cb = error_cb.clone(); let error_cb = error_cb.clone();
let client_nav_msg_cb = move |msg| { let client_nav_msg_cb = move |msg| {
@ -303,16 +272,13 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
html! { html! {
<ClientNav identity={ident_state.clone()} message_callback={client_nav_msg_cb} /> <ClientNav identity={ident_state.clone()} message_callback={client_nav_msg_cb} />
} }
}); };
let footer = dead_chat.not().then_some(html! {
<Footer />
});
html! { html! {
<> <>
{nav} {nav}
{content} {content}
{footer} <Footer />
</> </>
} }
} }

View File

@ -23,8 +23,7 @@ use gloo::net::websocket::{self, futures::WebSocket};
use instant::Instant; use instant::Instant;
use serde::Serialize; use serde::Serialize;
use thiserror::Error; use thiserror::Error;
use werewolves_proto::message::dead::DeadChatMessage; use werewolves_proto::message::{PlayerUpdate, ServerMessage};
use werewolves_proto::message::{ClientDeadChat, PlayerUpdate, ServerToClientMessage};
use werewolves_proto::{ use werewolves_proto::{
message::{ClientMessage, Identification, PublicIdentity}, message::{ClientMessage, Identification, PublicIdentity},
player::PlayerId, player::PlayerId,
@ -49,7 +48,6 @@ pub struct Connection2 {
ident: UseStateHandle<Option<(PlayerId, PublicIdentity)>>, ident: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
receiver: Rc<RefCell<UnboundedReceiver<ClientMessage>>>, receiver: Rc<RefCell<UnboundedReceiver<ClientMessage>>>,
active: Rc<RefCell<()>>, active: Rc<RefCell<()>>,
dead_chat: Option<Vec<DeadChatMessage>>,
} }
impl Connection2 { impl Connection2 {
@ -62,7 +60,6 @@ impl Connection2 {
state, state,
ident, ident,
receiver, receiver,
dead_chat: None,
active: Rc::new(RefCell::new(())), active: Rc::new(RefCell::new(())),
} }
} }
@ -159,9 +156,8 @@ impl Connection2 {
let mut ws = Self::connect_ws().await.fuse(); let mut ws = Self::connect_ws().await.fuse();
log::info!("connected to {url}"); log::info!("connected to {url}");
let ident = self.identification(); log::debug!("sending self ident");
log::debug!("sending self ident: {ident}"); if let Err(err) = ws.send(Self::encode_message(&self.identification())).await {
if let Err(err) = ws.send(Self::encode_message(&ident)).await {
log::error!("websocket identification send: {err}"); log::error!("websocket identification send: {err}");
continue 'outer; continue 'outer;
}; };
@ -231,7 +227,7 @@ impl Connection2 {
{ {
match msg { match msg {
websocket::Message::Text(text) => { websocket::Message::Text(text) => {
serde_json::from_str::<ServerToClientMessage>(&text) serde_json::from_str::<ServerMessage>(&text)
} }
websocket::Message::Bytes(items) => serde_json::from_slice(&items), websocket::Message::Bytes(items) => serde_json::from_slice(&items),
} }
@ -244,36 +240,14 @@ impl Connection2 {
continue; continue;
} }
websocket::Message::Bytes(bytes) => { websocket::Message::Bytes(bytes) => {
ciborium::from_reader::<ServerToClientMessage, _>(bytes.as_slice()) ciborium::from_reader::<ServerMessage, _>(bytes.as_slice())
} }
} }
} }
}; };
match parse { match parse {
Ok(ServerToClientMessage::DeadChat(msgs)) => {
self.dead_chat.replace(msgs.to_vec());
self.state.set(ClientEvent2::DeadChat {
messages: msgs.to_vec(),
});
}
Ok(ServerToClientMessage::DeadChatMessage(msg)) => {
if let Some(dead_chat) = self.dead_chat.as_mut() {
dead_chat.push(msg);
dead_chat.sort_by_key(|k| k.timestamp);
self.state.set(ClientEvent2::DeadChat {
messages: dead_chat.clone(),
});
} else if let Err(err) = ws
.send(Self::encode_message(&ClientMessage::DeadChat(
ClientDeadChat::GetHistory,
)))
.await
{
log::error!("sending dead chat history request: {err}");
}
}
Ok(msg) => { Ok(msg) => {
quit = matches!(msg, ServerToClientMessage::Disconnect); quit = matches!(msg, ServerMessage::Disconnect);
if let Some(state) = self.message_to_client_state(msg) { if let Some(state) = self.message_to_client_state(msg) {
self.state.set(state); self.state.set(state);
} }
@ -286,17 +260,13 @@ impl Connection2 {
} }
} }
fn message_to_client_state(&self, msg: ServerToClientMessage) -> Option<ClientEvent2> { fn message_to_client_state(&self, msg: ServerMessage) -> Option<ClientEvent2> {
log::debug!("received message: {msg:?}"); log::debug!("received message: {msg:?}");
Some(match msg { Some(match msg {
ServerToClientMessage::Error(err) => { ServerMessage::Story(story) => ClientEvent2::Story(story),
log::error!("server: {err}"); ServerMessage::Sleep => ClientEvent2::Sleep,
return None; ServerMessage::Disconnect => ClientEvent2::Disconnected,
} ServerMessage::LobbyInfo {
ServerToClientMessage::Story(story) => ClientEvent2::Story(story),
ServerToClientMessage::Sleep => ClientEvent2::Sleep,
ServerToClientMessage::Disconnect => ClientEvent2::Disconnected,
ServerToClientMessage::LobbyInfo {
joined, joined,
mut players, mut players,
} => { } => {
@ -307,16 +277,16 @@ impl Connection2 {
players: players.into_iter().collect(), players: players.into_iter().collect(),
} }
} }
ServerToClientMessage::GameStart { role } => ClientEvent2::ShowRole(role), ServerMessage::GameStart { role } => ClientEvent2::ShowRole(role),
ServerToClientMessage::InvalidMessageForGameState => { ServerMessage::InvalidMessageForGameState => {
log::error!("invalid message for game state"); log::error!("invalid message for game state");
return None; return None;
} }
ServerToClientMessage::NoSuchTarget => { ServerMessage::NoSuchTarget => {
log::error!("no such target"); log::error!("no such target");
return None; return None;
} }
ServerToClientMessage::Update(PlayerUpdate::Number(new_num)) => { ServerMessage::Update(PlayerUpdate::Number(new_num)) => {
let Some((pid, mut ident)) = (*self.ident).clone() else { let Some((pid, mut ident)) = (*self.ident).clone() else {
return None; return None;
}; };
@ -324,14 +294,11 @@ impl Connection2 {
self.ident.set(Some((pid, ident))); self.ident.set(Some((pid, ident)));
return None; return None;
} }
ServerToClientMessage::GameInProgress => ClientEvent2::GameInProgress, ServerMessage::GameInProgress => ClientEvent2::GameInProgress,
ServerToClientMessage::GameOver(_) | ServerToClientMessage::Reset => { ServerMessage::GameOver(_) | ServerMessage::Reset => {
log::info!("ignoring: {msg:?}"); log::info!("ignoring: {msg:?}");
return None; return None;
} }
ServerToClientMessage::DeadChat(_) | ServerToClientMessage::DeadChatMessage(_) => {
return None;
}
}) })
} }
} }

View File

@ -178,6 +178,7 @@ async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) {
Ok(ServerToHostMessage::Error(GameError::AwaitingResponse)) => {} Ok(ServerToHostMessage::Error(GameError::AwaitingResponse)) => {}
Ok(msg) => { Ok(msg) => {
log::debug!("got message: {:?}", msg.title()); log::debug!("got message: {:?}", msg.title());
log::trace!("message content: {msg:?}");
scope.send_message::<HostEvent>(msg.into()) scope.send_message::<HostEvent>(msg.into())
} }
Err(err) => { Err(err) => {
@ -323,6 +324,7 @@ impl Component for Host {
} }
fn view(&self, _ctx: &Context<Self>) -> Html { fn view(&self, _ctx: &Context<Self>) -> Html {
log::trace!("state: {:?}", self.state);
let content = match self.state.clone() { let content = match self.state.clone() {
HostState::ScreenOverrides { .. } => { HostState::ScreenOverrides { .. } => {
let send = { let send = {

View File

@ -1,219 +0,0 @@
// Copyright (C) 2026 Emilis Bliūdžius
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use chrono::{DateTime, Utc};
use chrono_humanize::Humanize;
use yew::prelude::*;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use web_sys::{HtmlElement, HtmlInputElement};
use werewolves_proto::message::{
PublicIdentity,
dead::{DeadChatContent, DeadChatMessage},
};
use crate::components::{Icon, IconSource, IconType, attributes::DiedToSpan};
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct DeadChatProperties {
pub on_send: Callback<String>,
pub messages: Vec<DeadChatMessage>,
}
#[function_component]
pub fn DeadChat(DeadChatProperties { on_send, messages }: &DeadChatProperties) -> Html {
let messages = messages
.iter()
.map(|m| {
html! {
<ChatMessage message={m.clone()} />
}
})
.collect::<Html>();
let submit = {
let on_send = on_send.clone();
move |ev: SubmitEvent| {
ev.prevent_default();
let Some(target) = ev.target_dyn_into::<HtmlElement>() else {
return;
};
let input = target
.query_selector("#message-input")
.expect_throw("could not find #message-input")
.expect_throw("could not find #message-input")
.dyn_into::<HtmlInputElement>()
.expect_throw("#message-input is not HtmlInputElement");
let value = input.value().trim().to_string();
if !value.is_empty() {
on_send.emit(value);
}
input.set_value("");
}
};
let node = use_node_ref();
use_effect_with(node.clone(), |node| {
let Some(div) = node.cast::<HtmlElement>() else {
log::warn!("chat-messages node not attached");
return;
};
let is_scrolled_to_bottom =
div.scroll_height() - div.client_height() <= div.scroll_top() + 1;
if !is_scrolled_to_bottom {
div.set_scroll_top(div.scroll_height() - div.client_height());
}
});
html! {
<div class="dead-chat">
<ol class="chat-messages" ref={node}>
{messages}
</ol>
<form onsubmit={submit}>
<input
type="text"
id="message-input"
placeholder="write to the dead"
autocomplete="off"
/>
<input type="submit" hidden=true/>
</form>
</div>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct DeadChatIdentProps {
pub ident: PublicIdentity,
}
#[function_component]
pub fn DeadChatIdent(DeadChatIdentProps { ident }: &DeadChatIdentProps) -> Html {
let pronouns = ident.pronouns.clone();
html! {
<span class="dead-ident" pronouns={pronouns}>
{ident.name.clone()}
</span>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct TimestampProps {
pub timestamp: DateTime<Utc>,
}
#[function_component]
pub fn Timestamp(TimestampProps { timestamp }: &TimestampProps) -> Html {
let use_relative = use_state(|| false);
let lock = use_state(|| false);
let enter = {
let lock = lock.clone();
let use_relative = use_relative.setter();
move |_| {
if !*lock {
use_relative.set(true);
}
}
};
let leave = {
let lock = lock.clone();
let use_relative = use_relative.setter();
move |_| {
if !*lock {
use_relative.set(false);
}
}
};
let bare_timestamp = timestamp
.naive_local()
.time()
.format("%H:%M:%S")
.to_string();
let timestamp_str = if *use_relative {
(*timestamp - Utc::now()).humanize()
} else {
bare_timestamp
};
let lock_set = {
let lock = lock.clone();
move |_| lock.set(!*lock)
};
html! {
<span
class="time"
onpointerenter={enter}
onpointercancel={leave.clone()}
onpointerleave={leave}
onclick={lock_set}
>
{timestamp_str}
</span>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct ChatMessageProps {
pub message: DeadChatMessage,
}
#[function_component]
pub fn ChatMessage(
ChatMessageProps {
message:
DeadChatMessage {
id: _,
timestamp,
message,
},
}: &ChatMessageProps,
) -> Html {
match message {
DeadChatContent::PlayerMessage { from, message } => {
html! {
<li class="message">
<Timestamp timestamp={*timestamp} />
<DeadChatIdent ident={from.clone().into_public()}/>
<span class="message-content">{message.clone()}</span>
</li>
}
}
DeadChatContent::Death { character, cause } => {
html! {
<li class="message">
<Timestamp timestamp={*timestamp} />
<Icon source={IconSource::Skull} icon_type={IconType::Fit}/>
<DeadChatIdent ident={character.clone().into_public()}/>
<span class="message-content">
{"died to "}
// <DiedToSpan died_to={cause.title()}/>
{cause.title().to_string()}
</span>
</li>
}
}
DeadChatContent::TimeChange(time) => {
html! {
<li class="message">
<span class="time-change">{time.to_string()}</span>
</li>
}
}
}
}

View File

@ -116,26 +116,12 @@ pub fn DaytimePlayerList(
} }
}) })
.collect::<Html>(); .collect::<Html>();
let (button_text, confirmation_text) = if marked.is_empty() { let button_text = if marked.is_empty() {
( "end day".to_string()
"end day".to_string(),
"really end the day with no executions?".to_string(),
)
} else if marked.len() == 1 { } else if marked.len() == 1 {
( "execute 1 player".to_string()
"execute 1 player".to_string(),
characters
.iter()
.find(|c| c.identity.character_id == marked[0])
.map(|c| c.identity.clone().into_public())
.map(|id| format!("really execute {id}?"))
.unwrap_or("really execute 1 player?".to_string()),
)
} else { } else {
( format!("execute {} players", marked.len())
format!("execute {} players", marked.len()),
format!("really execute {} players?", marked.len()),
)
}; };
let parity = { let parity = {
let wolves = characters let wolves = characters
@ -165,21 +151,10 @@ pub fn DaytimePlayerList(
.then_some(()) .then_some(())
.and_then(|_| on_execute.clone()) .and_then(|_| on_execute.clone())
.map(|on_execute| { .map(|on_execute| {
let on_execute = Callback::from(move |_| {
on_execute.emit(());
crate::components::modal::close_modal_by_id("execute");
});
html! { html! {
<crate::components::modal::Dialog <Button on_click={on_execute}>
id="execute" {button_text}
button={html!{{button_text.clone()}}} </Button>
close_button=false
>
<h3>{confirmation_text}</h3>
<Button on_click={on_execute}>
{button_text}
</Button>
</crate::components::modal::Dialog>
} }
}); });
let day = day.as_ref().map(|day| { let day = day.as_ref().map(|day| {

View File

@ -40,9 +40,6 @@ mod components {
pub mod story { pub mod story {
werewolves_macros::include_path!("werewolves/src/components/story"); werewolves_macros::include_path!("werewolves/src/components/story");
} }
pub mod chat {
werewolves_macros::include_path!("werewolves/src/components/chat");
}
} }
mod pages { mod pages {
werewolves_macros::include_path!("werewolves/src/pages"); werewolves_macros::include_path!("werewolves/src/pages");