Compare commits
No commits in common. "241420757e939b3e65ce1c994d7ba427703dfd9a" and "67d345646d880d5f5b9d4b0b867767f358fb62f4" have entirely different histories.
241420757e
...
67d345646d
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"))]
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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| {
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue