host gets dead chat too
This commit is contained in:
parent
27752727a3
commit
78ecb6c164
|
|
@ -83,6 +83,13 @@ impl Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dead_chats_since(&mut self, since: DateTime<Utc>) -> Vec<DeadChatMessage> {
|
||||||
|
match &mut self.state {
|
||||||
|
GameState::Day { village, .. } => village.dead_chat_mut().host_get_since(since),
|
||||||
|
GameState::Night { night } => night.dead_chat_mut().host_get_since(since),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_dead_chat_request(
|
pub fn process_dead_chat_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
player_id: PlayerId,
|
player_id: PlayerId,
|
||||||
|
|
@ -106,7 +113,10 @@ impl Game {
|
||||||
GameState::Day { village, .. } => {
|
GameState::Day { village, .. } => {
|
||||||
village.send_dead_chat_message(msg.clone())?
|
village.send_dead_chat_message(msg.clone())?
|
||||||
}
|
}
|
||||||
GameState::Night { night } => night.send_dead_chat_message(msg.clone())?,
|
GameState::Night { night } => {
|
||||||
|
let time = night.village().time();
|
||||||
|
night.dead_chat_mut().add(time, msg.clone())?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(ServerToClientMessage::DeadChatMessage(msg))
|
Ok(ServerToClientMessage::DeadChatMessage(msg))
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +148,28 @@ impl Game {
|
||||||
|
|
||||||
pub fn process(&mut self, message: HostGameMessage) -> Result<ServerToHostMessage> {
|
pub fn process(&mut self, message: HostGameMessage) -> Result<ServerToHostMessage> {
|
||||||
match (&mut self.state, message) {
|
match (&mut self.state, message) {
|
||||||
|
(_, HostGameMessage::SendChatMessage(msg)) => {
|
||||||
|
let msg = match &mut self.state {
|
||||||
|
GameState::Day { village, .. } => {
|
||||||
|
let time = village.time();
|
||||||
|
village.dead_chat_mut().add_host_message(time, msg)
|
||||||
|
}
|
||||||
|
GameState::Night { night } => {
|
||||||
|
let time = night.village().time();
|
||||||
|
night.dead_chat_mut().add_host_message(time, msg)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::info!("host sent dead chat message: {msg:?}");
|
||||||
|
|
||||||
|
Ok(ServerToHostMessage::DeadChatMessage(msg))
|
||||||
|
}
|
||||||
|
(_, HostGameMessage::GetDeadChatSince(since)) => Ok(ServerToHostMessage::DeadChat(
|
||||||
|
match &mut self.state {
|
||||||
|
GameState::Day { village, .. } => village.dead_chat(),
|
||||||
|
GameState::Night { night } => night.dead_chat(),
|
||||||
|
}
|
||||||
|
.host_get_since(since),
|
||||||
|
)),
|
||||||
(GameState::Night { night }, HostGameMessage::SeePlayersWithRoles) => {
|
(GameState::Night { night }, HostGameMessage::SeePlayersWithRoles) => {
|
||||||
Ok(ServerToHostMessage::PlayerStates(
|
Ok(ServerToHostMessage::PlayerStates(
|
||||||
night
|
night
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ use crate::{
|
||||||
night::changes::{ChangesLookup, NightChange},
|
night::changes::{ChangesLookup, NightChange},
|
||||||
},
|
},
|
||||||
message::{
|
message::{
|
||||||
dead::DeadChatMessage,
|
dead::{DeadChat, DeadChatMessage},
|
||||||
night::{
|
night::{
|
||||||
ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, ActionType, Visits,
|
ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, ActionType, Visits,
|
||||||
},
|
},
|
||||||
|
|
@ -1224,8 +1224,12 @@ impl Night {
|
||||||
&self.village
|
&self.village
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_dead_chat_message(&mut self, msg: DeadChatMessage) -> Result<()> {
|
pub fn dead_chat(&self) -> &DeadChat {
|
||||||
self.village.send_dead_chat_message(msg)
|
self.village.dead_chat()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dead_chat_mut(&mut self) -> &mut DeadChat {
|
||||||
|
self.village.dead_chat_mut()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
mod apply;
|
mod apply;
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -65,10 +66,22 @@ impl Village {
|
||||||
&self.dead_chat
|
&self.dead_chat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn dead_chat_mut(&mut self) -> &mut DeadChat {
|
||||||
|
&mut self.dead_chat
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_dead_chat_message(&mut self, msg: DeadChatMessage) -> Result<()> {
|
pub fn send_dead_chat_message(&mut self, msg: DeadChatMessage) -> Result<()> {
|
||||||
self.dead_chat.add(self.time, msg)
|
self.dead_chat.add(self.time, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn host_send_dead_chat_message(&mut self, msg: String) -> DeadChatMessage {
|
||||||
|
self.dead_chat.add_host_message(self.time, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn host_dead_chat_since(&mut self, since: DateTime<Utc>) -> Vec<DeadChatMessage> {
|
||||||
|
self.dead_chat.host_get_since(since)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn settings(&self) -> GameSettings {
|
pub fn settings(&self) -> GameSettings {
|
||||||
self.settings.clone()
|
self.settings.clone()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -33,6 +33,7 @@ type Result<T> = core::result::Result<T, GameError>;
|
||||||
pub struct DeadChat {
|
pub struct DeadChat {
|
||||||
deaths: Vec<(GameTime, CharacterId)>,
|
deaths: Vec<(GameTime, CharacterId)>,
|
||||||
messages: HashMap<GameTime, Vec<DeadChatMessage>>,
|
messages: HashMap<GameTime, Vec<DeadChatMessage>>,
|
||||||
|
new_messages_from: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeadChat {
|
impl DeadChat {
|
||||||
|
|
@ -49,6 +50,7 @@ impl DeadChat {
|
||||||
Self {
|
Self {
|
||||||
messages,
|
messages,
|
||||||
deaths: Vec::new(),
|
deaths: Vec::new(),
|
||||||
|
new_messages_from: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +65,10 @@ impl DeadChat {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, time: GameTime, msg: DeadChatMessage) -> Result<()> {
|
pub fn add(&mut self, time: GameTime, msg: DeadChatMessage) -> Result<()> {
|
||||||
|
let start = msg
|
||||||
|
.timestamp
|
||||||
|
.checked_sub_signed(TimeDelta::nanoseconds(1))
|
||||||
|
.unwrap_or_default();
|
||||||
if !self
|
if !self
|
||||||
.deaths
|
.deaths
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -75,9 +81,28 @@ impl DeadChat {
|
||||||
} else {
|
} else {
|
||||||
self.messages.insert(time, vec![msg]);
|
self.messages.insert(time, vec![msg]);
|
||||||
}
|
}
|
||||||
|
self.new_messages_from.get_or_insert(start);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_host_message(&mut self, time: GameTime, msg: String) -> DeadChatMessage {
|
||||||
|
let start = Utc::now();
|
||||||
|
let msg = DeadChatMessage {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
message: DeadChatContent::HostMessage(msg),
|
||||||
|
timestamp: start
|
||||||
|
.checked_add_signed(TimeDelta::nanoseconds(1))
|
||||||
|
.unwrap_or_else(Utc::now),
|
||||||
|
};
|
||||||
|
if let Some(msgs) = self.messages.get_mut(&time) {
|
||||||
|
msgs.push(msg.clone());
|
||||||
|
} else {
|
||||||
|
self.messages.insert(time, vec![msg.clone()]);
|
||||||
|
}
|
||||||
|
self.new_messages_from.get_or_insert(start);
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_dead(
|
pub fn set_dead(
|
||||||
&mut self,
|
&mut self,
|
||||||
dead: impl Iterator<Item = (GameTime, Character)> + Clone,
|
dead: impl Iterator<Item = (GameTime, Character)> + Clone,
|
||||||
|
|
@ -129,6 +154,23 @@ impl DeadChat {
|
||||||
) {
|
) {
|
||||||
log::warn!("replaced: {existing:?}");
|
log::warn!("replaced: {existing:?}");
|
||||||
}
|
}
|
||||||
|
self.new_messages_from.get_or_insert(Utc::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_messages_since(&mut self) -> Option<DateTime<Utc>> {
|
||||||
|
self.new_messages_from.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn host_get_since(&self, t: DateTime<Utc>) -> Vec<DeadChatMessage> {
|
||||||
|
let mut messages = self
|
||||||
|
.messages
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(_, msgs)| msgs.into_iter())
|
||||||
|
.filter(|m| m.timestamp >= t)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
messages.sort_by_key(|m| m.timestamp);
|
||||||
|
messages
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_since(&self, t: DateTime<Utc>, character: CharacterId) -> Box<[DeadChatMessage]> {
|
pub fn get_since(&self, t: DateTime<Utc>, character: CharacterId) -> Box<[DeadChatMessage]> {
|
||||||
|
|
@ -173,6 +215,7 @@ pub enum DeadChatContent {
|
||||||
cause: DiedTo,
|
cause: DiedTo,
|
||||||
},
|
},
|
||||||
TimeChange(GameTime),
|
TimeChange(GameTime),
|
||||||
|
HostMessage(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeadChatContent {
|
impl DeadChatContent {
|
||||||
|
|
@ -180,13 +223,14 @@ impl DeadChatContent {
|
||||||
match self {
|
match self {
|
||||||
DeadChatContent::PlayerMessage { from, .. } => from.character_id == character_id,
|
DeadChatContent::PlayerMessage { from, .. } => from.character_id == character_id,
|
||||||
DeadChatContent::Death { character, .. } => character.character_id == character_id,
|
DeadChatContent::Death { character, .. } => character.character_id == character_id,
|
||||||
DeadChatContent::TimeChange(_) => false,
|
DeadChatContent::TimeChange(_) | DeadChatContent::HostMessage(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn message(&self) -> Option<&str> {
|
pub const fn message(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
DeadChatContent::PlayerMessage { message, .. } => Some(message.as_str()),
|
DeadChatContent::HostMessage(message)
|
||||||
|
| DeadChatContent::PlayerMessage { message, .. } => Some(message.as_str()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
// 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::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -22,6 +23,7 @@ use crate::{
|
||||||
game::{GameOver, GameSettings, story::GameStory},
|
game::{GameOver, GameSettings, story::GameStory},
|
||||||
message::{
|
message::{
|
||||||
CharacterIdentity,
|
CharacterIdentity,
|
||||||
|
dead::DeadChatMessage,
|
||||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
},
|
},
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
|
|
@ -48,6 +50,8 @@ pub enum PostGameMessage {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum HostGameMessage {
|
pub enum HostGameMessage {
|
||||||
|
SendChatMessage(String),
|
||||||
|
GetDeadChatSince(DateTime<Utc>),
|
||||||
Day(HostDayMessage),
|
Day(HostDayMessage),
|
||||||
Night(HostNightMessage),
|
Night(HostNightMessage),
|
||||||
PreviousState,
|
PreviousState,
|
||||||
|
|
@ -113,4 +117,6 @@ pub enum ServerToHostMessage {
|
||||||
story: GameStory,
|
story: GameStory,
|
||||||
page: usize,
|
page: usize,
|
||||||
},
|
},
|
||||||
|
DeadChat(Vec<DeadChatMessage>),
|
||||||
|
DeadChatMessage(DeadChatMessage),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ use crate::{
|
||||||
lobby::{Lobby, LobbyPlayers},
|
lobby::{Lobby, LobbyPlayers},
|
||||||
runner::{ClientUpdate, IdentifiedClientMessage, Message},
|
runner::{ClientUpdate, IdentifiedClientMessage, Message},
|
||||||
};
|
};
|
||||||
|
use chrono::Utc;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
character::Character,
|
character::Character,
|
||||||
|
|
@ -259,6 +260,7 @@ impl GameRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn next(&mut self) -> Option<GameOver> {
|
pub async fn next(&mut self) -> Option<GameOver> {
|
||||||
|
let start = Utc::now();
|
||||||
let msg = match self.comms.message().await {
|
let msg = match self.comms.message().await {
|
||||||
Ok(Message::Client(IdentifiedClientMessage {
|
Ok(Message::Client(IdentifiedClientMessage {
|
||||||
update: ClientUpdate::ConnectStateUpdate,
|
update: ClientUpdate::ConnectStateUpdate,
|
||||||
|
|
@ -343,30 +345,48 @@ impl GameRunner {
|
||||||
};
|
};
|
||||||
|
|
||||||
let pre_time = self.game.village().time();
|
let pre_time = self.game.village().time();
|
||||||
|
let mut is_host_message = false;
|
||||||
match self.host_message(msg) {
|
match self.host_message(msg) {
|
||||||
|
Ok(ServerToHostMessage::DeadChatMessage(msg)) => {
|
||||||
|
self.comms
|
||||||
|
.host()
|
||||||
|
.send(ServerToHostMessage::DeadChatMessage(msg))
|
||||||
|
.log_err();
|
||||||
|
is_host_message = true;
|
||||||
|
}
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
self.comms.host().send(resp).log_warn();
|
self.comms.host().send(resp).log_err();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.comms
|
self.comms
|
||||||
.host()
|
.host()
|
||||||
.send(ServerToHostMessage::Error(err))
|
.send(ServerToHostMessage::Error(err))
|
||||||
.log_warn();
|
.log_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let messages_for_host = self.game.dead_chats_since(start);
|
||||||
|
let msg_count = messages_for_host.len();
|
||||||
|
if !messages_for_host.is_empty()
|
||||||
|
&& let Err(err) = self
|
||||||
|
.comms
|
||||||
|
.host()
|
||||||
|
.send(ServerToHostMessage::DeadChat(messages_for_host))
|
||||||
|
{
|
||||||
|
log::error!("sending {msg_count} dead chat messages to host channel: {err}");
|
||||||
|
}
|
||||||
let post_time = self.game.village().time();
|
let post_time = self.game.village().time();
|
||||||
if let Some(game_over) = self.game.game_over() {
|
if let Some(game_over) = self.game.game_over() {
|
||||||
return Some(game_over);
|
return Some(game_over);
|
||||||
}
|
}
|
||||||
if pre_time != post_time {
|
if pre_time != post_time || is_host_message {
|
||||||
let newly_dead = self
|
let dead = self
|
||||||
.game
|
.game
|
||||||
.village()
|
.village()
|
||||||
.dead_characters()
|
.dead_characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| c.died_to().map(|_| (c.character_id(), c.player_id())));
|
.filter_map(|c| c.died_to().map(|_| (c.character_id(), c.player_id())));
|
||||||
|
|
||||||
for (char, player) in newly_dead {
|
for (char, player) in dead {
|
||||||
let msgs = self
|
let msgs = self
|
||||||
.game
|
.game
|
||||||
.village()
|
.village()
|
||||||
|
|
@ -381,6 +401,13 @@ impl GameRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_dead_message(&mut self, msg: &DeadChatMessage) -> Result<()> {
|
pub async fn send_dead_message(&mut self, msg: &DeadChatMessage) -> Result<()> {
|
||||||
|
if let Err(err) = self
|
||||||
|
.comms
|
||||||
|
.host()
|
||||||
|
.send(ServerToHostMessage::DeadChatMessage(msg.clone()))
|
||||||
|
{
|
||||||
|
log::error!("sending message {} to host channel: {err}", msg.id);
|
||||||
|
}
|
||||||
let player_ids = self
|
let player_ids = self
|
||||||
.game
|
.game
|
||||||
.village()
|
.village()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ web-sys = { version = "0.3", features = [
|
||||||
"HtmlSelectElement",
|
"HtmlSelectElement",
|
||||||
"HtmlDialogElement",
|
"HtmlDialogElement",
|
||||||
"DomRect",
|
"DomRect",
|
||||||
|
"WheelEvent",
|
||||||
] }
|
] }
|
||||||
wasm-bindgen = { version = "=0.2.100" }
|
wasm-bindgen = { version = "=0.2.100" }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,9 @@ body {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
color: rgba(255, 255, 255, 1);
|
color: rgba(255, 255, 255, 1);
|
||||||
background: black;
|
background: black;
|
||||||
|
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.7) black;
|
||||||
}
|
}
|
||||||
|
|
||||||
app {
|
app {
|
||||||
|
|
@ -128,6 +131,10 @@ $error_shadow_color: hsla(340, 95%, 61%, 0.7);
|
||||||
$error_shadow_color_2: hsla(0, 95%, 61%, 0.7);
|
$error_shadow_color_2: hsla(0, 95%, 61%, 0.7);
|
||||||
$error_filter: drop-shadow(5px 5px 0 $error_shadow_color) drop-shadow(5px 5px 0 $error_shadow_color_2);
|
$error_filter: drop-shadow(5px 5px 0 $error_shadow_color) drop-shadow(5px 5px 0 $error_shadow_color_2);
|
||||||
|
|
||||||
|
$host_nav_height: 36px;
|
||||||
|
$host_nav_top_pad: 10px;
|
||||||
|
$host_nav_bottom_pad: 10px;
|
||||||
|
$host_nav_total_height: $host_nav_height + $host_nav_top_pad + $host_nav_bottom_pad;
|
||||||
|
|
||||||
nav.host-nav {
|
nav.host-nav {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|
@ -140,7 +147,10 @@ nav.host-nav {
|
||||||
padding-left: 5vw;
|
padding-left: 5vw;
|
||||||
padding-right: 5vw;
|
padding-right: 5vw;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
height: $host_nav_height;
|
||||||
|
overflow-x: scroll;
|
||||||
|
scrollbar-width: none;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2093,7 +2103,6 @@ li.choice {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
max-height: 40%;
|
|
||||||
|
|
||||||
@media only screen and (min-width : 1600px) {
|
@media only screen and (min-width : 1600px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -2956,6 +2965,10 @@ dialog {
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
|
&:first-child {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
user-select: text;
|
user-select: text;
|
||||||
padding-inline-start: 0px;
|
padding-inline-start: 0px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
@ -2965,10 +2978,7 @@ dialog {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 95vh;
|
max-height: 95vh;
|
||||||
justify-content: flex-end;
|
|
||||||
// scrollbar-width: thin;
|
|
||||||
scrollbar-width: none;
|
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.7) black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
|
@ -3127,3 +3137,8 @@ dialog {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 1ch;
|
gap: 1ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.host-dead-chat {
|
||||||
|
height: calc(100vh - $host_nav_total_height);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ use werewolves_proto::{
|
||||||
game::{GameOver, GameSettings, story::GameStory},
|
game::{GameOver, GameSettings, story::GameStory},
|
||||||
message::{
|
message::{
|
||||||
CharacterIdentity, CharacterState, PlayerState, PublicIdentity,
|
CharacterIdentity, CharacterState, PlayerState, PublicIdentity,
|
||||||
|
dead::DeadChatMessage,
|
||||||
host::{
|
host::{
|
||||||
HostDayMessage, HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
|
HostDayMessage, HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
|
||||||
PostGameMessage, ServerToHostMessage,
|
PostGameMessage, ServerToHostMessage,
|
||||||
|
|
@ -44,11 +45,13 @@ use crate::{
|
||||||
components::{
|
components::{
|
||||||
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Victory,
|
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Victory,
|
||||||
action::{ActionResultView, Prompt},
|
action::{ActionResultView, Prompt},
|
||||||
|
chat::DeadChat,
|
||||||
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup, VotingMode},
|
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup, VotingMode},
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
story::Story,
|
story::Story,
|
||||||
},
|
},
|
||||||
pages::RolePage,
|
pages::RolePage,
|
||||||
|
scroll::vertical_scroll_to_horizontal,
|
||||||
storage::StorageKey,
|
storage::StorageKey,
|
||||||
test_util::TestScreens,
|
test_util::TestScreens,
|
||||||
};
|
};
|
||||||
|
|
@ -204,6 +207,8 @@ pub enum HostEvent {
|
||||||
ToOverrideView,
|
ToOverrideView,
|
||||||
ReturnFromOverride,
|
ReturnFromOverride,
|
||||||
ExpectEcho(Box<HostEvent>),
|
ExpectEcho(Box<HostEvent>),
|
||||||
|
DeadChat(Vec<DeadChatMessage>),
|
||||||
|
DeadChatMessage(DeadChatMessage),
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, PartialEq, Titles)]
|
#[derive(Debug, Clone, PartialEq, Titles)]
|
||||||
pub enum HostState {
|
pub enum HostState {
|
||||||
|
|
@ -240,11 +245,16 @@ pub enum HostState {
|
||||||
page: usize,
|
page: usize,
|
||||||
},
|
},
|
||||||
CharacterStates(Box<[CharacterState]>),
|
CharacterStates(Box<[CharacterState]>),
|
||||||
|
InChat {
|
||||||
|
previously: Box<HostState>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ServerToHostMessage> for HostEvent {
|
impl From<ServerToHostMessage> for HostEvent {
|
||||||
fn from(msg: ServerToHostMessage) -> Self {
|
fn from(msg: ServerToHostMessage) -> Self {
|
||||||
match msg {
|
match msg {
|
||||||
|
ServerToHostMessage::DeadChatMessage(msg) => HostEvent::DeadChatMessage(msg),
|
||||||
|
ServerToHostMessage::DeadChat(msgs) => HostEvent::DeadChat(msgs),
|
||||||
ServerToHostMessage::PlayerStates(states) => HostEvent::CharacterList(states),
|
ServerToHostMessage::PlayerStates(states) => HostEvent::CharacterList(states),
|
||||||
ServerToHostMessage::QrMode(mode) => HostEvent::QrMode(mode),
|
ServerToHostMessage::QrMode(mode) => HostEvent::QrMode(mode),
|
||||||
ServerToHostMessage::Disconnect => HostEvent::SetState(HostState::Disconnected),
|
ServerToHostMessage::Disconnect => HostEvent::SetState(HostState::Disconnected),
|
||||||
|
|
@ -292,6 +302,7 @@ pub struct Host {
|
||||||
qr_mode: bool,
|
qr_mode: bool,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
expecting_echo: Option<HostEvent>,
|
expecting_echo: Option<HostEvent>,
|
||||||
|
dead_chat: Option<Vec<DeadChatMessage>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Host {
|
impl Component for Host {
|
||||||
|
|
@ -323,11 +334,67 @@ impl Component for Host {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
expecting_echo: None,
|
expecting_echo: None,
|
||||||
|
dead_chat: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||||
|
if self.dead_chat.is_none()
|
||||||
|
&& matches!(&self.state, HostState::Day { .. } | HostState::Prompt(_, _))
|
||||||
|
{
|
||||||
|
let mut send = self.send.clone();
|
||||||
|
let on_err = self.error_callback.clone();
|
||||||
|
yew::platform::spawn_local(async move {
|
||||||
|
if let Err(err) = send
|
||||||
|
.send(HostMessage::InGame(HostGameMessage::GetDeadChatSince(
|
||||||
|
Default::default(),
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
on_err.emit(Some(WerewolfError::Send(err)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let content = match self.state.clone() {
|
let content = match self.state.clone() {
|
||||||
|
HostState::InChat { .. } => {
|
||||||
|
let on_send = {
|
||||||
|
let send = self.send.clone();
|
||||||
|
let on_err = self.error_callback.clone();
|
||||||
|
move |msg: String| {
|
||||||
|
let mut send = send.clone();
|
||||||
|
let on_err = on_err.clone();
|
||||||
|
yew::platform::spawn_local(async move {
|
||||||
|
if let Err(err) = send
|
||||||
|
.send(HostMessage::InGame(HostGameMessage::SendChatMessage(msg)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
on_err.emit(Some(WerewolfError::Send(err)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let messages = self.dead_chat.clone().unwrap_or_default();
|
||||||
|
if messages.is_empty() {
|
||||||
|
let mut send = self.send.clone();
|
||||||
|
let on_err = self.error_callback.clone();
|
||||||
|
yew::platform::spawn_local(async move {
|
||||||
|
if let Err(err) = send
|
||||||
|
.send(HostMessage::InGame(HostGameMessage::GetDeadChatSince(
|
||||||
|
Default::default(),
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
on_err.emit(Some(WerewolfError::Send(err)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
html! {
|
||||||
|
<div class="host-dead-chat">
|
||||||
|
<DeadChat on_send={on_send} messages={messages}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
HostState::VotingMode { characters, .. } => {
|
HostState::VotingMode { characters, .. } => {
|
||||||
html! {
|
html! {
|
||||||
<VotingMode characters={characters.clone()}/>
|
<VotingMode characters={characters.clone()}/>
|
||||||
|
|
@ -508,56 +575,66 @@ impl Component for Host {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let voting_mode_btn = {
|
let mut nav_buttons = Vec::<Html>::new();
|
||||||
match &self.state {
|
let dead_chat_btn = {
|
||||||
HostState::Day { characters, .. } => {
|
let on_dead_chat = {
|
||||||
let on_vote_mode = {
|
let scope = _ctx.link().clone();
|
||||||
let scope = _ctx.link().clone();
|
let state = self.state.clone();
|
||||||
let state = self.state.clone();
|
move |_| {
|
||||||
let characters = characters.clone();
|
scope.send_message(HostEvent::SetState(HostState::InChat {
|
||||||
move |_| {
|
previously: Box::new(state.clone()),
|
||||||
scope.send_message(HostEvent::SetState(HostState::VotingMode {
|
}))
|
||||||
return_to: Box::new(state.clone()),
|
|
||||||
characters: characters.clone(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(html! {
|
|
||||||
<Button on_click={on_vote_mode}>{"voting mode"}</Button>
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
HostState::VotingMode { .. } => {
|
};
|
||||||
let back =
|
html! {
|
||||||
crate::callback::send_message(HostMessage::GetState, self.send.clone());
|
<Button on_click={on_dead_chat}>{"chat"}</Button>
|
||||||
Some(html! {
|
|
||||||
<Button on_click={back}>{"back"}</Button>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let view_roles_btn = match &self.state {
|
match &self.state {
|
||||||
|
HostState::Day { characters, .. } => {
|
||||||
|
let on_vote_mode = {
|
||||||
|
let scope = _ctx.link().clone();
|
||||||
|
let state = self.state.clone();
|
||||||
|
let characters = characters.clone();
|
||||||
|
move |_| {
|
||||||
|
scope.send_message(HostEvent::SetState(HostState::VotingMode {
|
||||||
|
return_to: Box::new(state.clone()),
|
||||||
|
characters: characters.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nav_buttons.push(html! {
|
||||||
|
<Button on_click={on_vote_mode}>{"voting mode"}</Button>
|
||||||
|
});
|
||||||
|
|
||||||
|
nav_buttons.push(dead_chat_btn);
|
||||||
|
}
|
||||||
|
HostState::VotingMode { .. } => {
|
||||||
|
let back = crate::callback::send_message(HostMessage::GetState, self.send.clone());
|
||||||
|
nav_buttons.push(html! {
|
||||||
|
<Button on_click={back}>{"back"}</Button>
|
||||||
|
});
|
||||||
|
}
|
||||||
HostState::Prompt(_, _) | HostState::Result(_, _) => {
|
HostState::Prompt(_, _) | HostState::Result(_, _) => {
|
||||||
|
let on_prev_click = callback::send_message(
|
||||||
|
HostMessage::InGame(HostGameMessage::PreviousState),
|
||||||
|
self.send.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
nav_buttons.push(html! {
|
||||||
|
<Button on_click={on_prev_click}>{"previous"}</Button>
|
||||||
|
});
|
||||||
|
|
||||||
let on_view_click = crate::callback::send_message(
|
let on_view_click = crate::callback::send_message(
|
||||||
HostMessage::InGame(HostGameMessage::SeePlayersWithRoles),
|
HostMessage::InGame(HostGameMessage::SeePlayersWithRoles),
|
||||||
self.send.clone(),
|
self.send.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Some(html! {
|
nav_buttons.push(html! {
|
||||||
<Button on_click={on_view_click}>{"view players"}</Button>
|
<Button on_click={on_view_click}>{"view players"}</Button>
|
||||||
})
|
});
|
||||||
}
|
|
||||||
HostState::CharacterStates(_) => {
|
|
||||||
let back = crate::callback::send_message(HostMessage::GetState, self.send.clone());
|
|
||||||
Some(html! {
|
|
||||||
<Button on_click={back}>{"back"}</Button>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let override_screens_btn = match &self.state {
|
|
||||||
HostState::Prompt(_, _) | HostState::Result(_, _) => {
|
|
||||||
let overrides_click = {
|
let overrides_click = {
|
||||||
let scope = _ctx.link().clone();
|
let scope = _ctx.link().clone();
|
||||||
Callback::from(move |_| {
|
Callback::from(move |_| {
|
||||||
|
|
@ -565,39 +642,12 @@ impl Component for Host {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(html! {
|
nav_buttons.push(html! {
|
||||||
<Button on_click={overrides_click}>{"overrides"}</Button>
|
<Button on_click={overrides_click}>{"overrides"}</Button>
|
||||||
})
|
});
|
||||||
}
|
|
||||||
HostState::ScreenOverrides { .. } => {
|
|
||||||
let return_click = {
|
|
||||||
let scope = _ctx.link().clone();
|
|
||||||
Callback::from(move |_| {
|
|
||||||
scope.send_message(HostEvent::ReturnFromOverride);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(html! {
|
nav_buttons.push(dead_chat_btn);
|
||||||
<Button on_click={return_click}>{"back"}</Button>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let previous_btn = match &self.state {
|
|
||||||
HostState::Prompt(_, _) | HostState::Result(_, _) => {
|
|
||||||
let on_prev_click = callback::send_message(
|
|
||||||
HostMessage::InGame(HostGameMessage::PreviousState),
|
|
||||||
self.send.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(html! {
|
|
||||||
<Button on_click={on_prev_click}>{"previous"}</Button>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let skip_btn = match &self.state {
|
|
||||||
HostState::Prompt(_, _) | HostState::Result(_, _) => {
|
|
||||||
let on_skip_click = callback::send_message(
|
let on_skip_click = callback::send_message(
|
||||||
HostMessage::InGame(HostGameMessage::Night(HostNightMessage::SkipAction)),
|
HostMessage::InGame(HostGameMessage::Night(HostNightMessage::SkipAction)),
|
||||||
self.send.clone(),
|
self.send.clone(),
|
||||||
|
|
@ -609,7 +659,7 @@ impl Component for Host {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(html! {
|
nav_buttons.push(html! {
|
||||||
<crate::components::modal::Dialog
|
<crate::components::modal::Dialog
|
||||||
id={"skip-button"}
|
id={"skip-button"}
|
||||||
button={html!{{"skip"}}}
|
button={html!{{"skip"}}}
|
||||||
|
|
@ -619,18 +669,47 @@ impl Component for Host {
|
||||||
<p>{"if this is the final prompt of the night, you may not be able to go back"}</p>
|
<p>{"if this is the final prompt of the night, you may not be able to go back"}</p>
|
||||||
<Button on_click={on_skip_click}>{"skip prompt"}</Button>
|
<Button on_click={on_skip_click}>{"skip prompt"}</Button>
|
||||||
</crate::components::modal::Dialog>
|
</crate::components::modal::Dialog>
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
_ => None,
|
HostState::CharacterStates(_) => {
|
||||||
};
|
let back = crate::callback::send_message(HostMessage::GetState, self.send.clone());
|
||||||
|
nav_buttons.push(html! {
|
||||||
|
<Button on_click={back}>{"back"}</Button>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HostState::ScreenOverrides { .. } => {
|
||||||
|
let return_click = {
|
||||||
|
let scope = _ctx.link().clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
scope.send_message(HostEvent::ReturnFromOverride);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
nav_buttons.push(html! {
|
||||||
|
<Button on_click={return_click}>{"back"}</Button>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HostState::InChat { previously } => {
|
||||||
|
let return_click = {
|
||||||
|
let scope = _ctx.link().clone();
|
||||||
|
let previously = (**previously).clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
scope.send_message(HostEvent::SetState(previously.clone()));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
nav_buttons.push(html! {
|
||||||
|
<Button on_click={return_click}>{"back"}</Button>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
let on_wheel = vertical_scroll_to_horizontal(".host-nav");
|
||||||
|
|
||||||
let nav = self.big_screen.not().then(|| {
|
let nav = self.big_screen.not().then(|| {
|
||||||
html! {
|
html! {
|
||||||
<nav class="host-nav" style="z-index: 3;">
|
<nav class="host-nav" style="z-index: 3;" onwheel={on_wheel}>
|
||||||
{previous_btn}
|
{nav_buttons}
|
||||||
{view_roles_btn}
|
|
||||||
{override_screens_btn}
|
|
||||||
{skip_btn}
|
|
||||||
{voting_mode_btn}
|
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -736,8 +815,41 @@ impl Component for Host {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Host {
|
impl Host {
|
||||||
fn message_to_new_state(&self, msg: HostEvent) -> (Option<HostState>, bool) {
|
fn message_to_new_state(&mut self, msg: HostEvent) -> (Option<HostState>, bool) {
|
||||||
match msg {
|
match msg {
|
||||||
|
HostEvent::DeadChatMessage(msg) => {
|
||||||
|
match self.dead_chat.as_mut() {
|
||||||
|
Some(dc) => {
|
||||||
|
dc.push(msg);
|
||||||
|
dc.sort_by_key(|d| d.timestamp);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.dead_chat.replace(vec![msg]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let HostState::InChat { .. } = &self.state {
|
||||||
|
(None, true)
|
||||||
|
} else {
|
||||||
|
(None, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HostEvent::DeadChat(mut msgs) => {
|
||||||
|
match self.dead_chat.as_mut() {
|
||||||
|
Some(chat) => {
|
||||||
|
chat.append(&mut msgs);
|
||||||
|
chat.sort_by_key(|k| k.timestamp);
|
||||||
|
chat.dedup_by_key(|k| k.id);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.dead_chat.replace(msgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let HostState::InChat { .. } = &self.state {
|
||||||
|
(None, true)
|
||||||
|
} else {
|
||||||
|
(None, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
HostEvent::ExpectEcho(_) => (None, false),
|
HostEvent::ExpectEcho(_) => (None, false),
|
||||||
HostEvent::ReturnFromOverride => {
|
HostEvent::ReturnFromOverride => {
|
||||||
if let HostState::ScreenOverrides { return_to } = &self.state {
|
if let HostState::ScreenOverrides { return_to } = &self.state {
|
||||||
|
|
@ -776,6 +888,7 @@ impl Host {
|
||||||
.unwrap_or(LAST)
|
.unwrap_or(LAST)
|
||||||
.cmp(&r.identification.public.number.unwrap_or(LAST))
|
.cmp(&r.identification.public.number.unwrap_or(LAST))
|
||||||
});
|
});
|
||||||
|
self.dead_chat = None;
|
||||||
(
|
(
|
||||||
Some(HostState::Lobby {
|
Some(HostState::Lobby {
|
||||||
settings,
|
settings,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ use werewolves_proto::message::{
|
||||||
dead::{DeadChatContent, DeadChatMessage},
|
dead::{DeadChatContent, DeadChatMessage},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::components::{Icon, IconSource, IconType, attributes::DiedToSpan};
|
use crate::components::{Icon, IconSource, IconType};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct DeadChatProperties {
|
pub struct DeadChatProperties {
|
||||||
|
|
@ -185,6 +185,18 @@ pub fn ChatMessage(
|
||||||
}: &ChatMessageProps,
|
}: &ChatMessageProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
match message {
|
match message {
|
||||||
|
DeadChatContent::HostMessage(message) => {
|
||||||
|
html! {
|
||||||
|
<li class="message">
|
||||||
|
<Timestamp timestamp={*timestamp} />
|
||||||
|
<span class="red">
|
||||||
|
// <Icon source={IconSource::Adjudicator} icon_type={IconType::Fit}/>
|
||||||
|
<strong>{"Host"}</strong>
|
||||||
|
</span>
|
||||||
|
<span class="message-content">{message.clone()}</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
DeadChatContent::PlayerMessage { from, message } => {
|
DeadChatContent::PlayerMessage { from, message } => {
|
||||||
html! {
|
html! {
|
||||||
<li class="message">
|
<li class="message">
|
||||||
|
|
@ -202,7 +214,6 @@ pub fn ChatMessage(
|
||||||
<DeadChatIdent ident={character.clone().into_public()}/>
|
<DeadChatIdent ident={character.clone().into_public()}/>
|
||||||
<span class="message-content">
|
<span class="message-content">
|
||||||
{"died to "}
|
{"died to "}
|
||||||
// <DiedToSpan died_to={cause.title()}/>
|
|
||||||
{cause.title().to_string()}
|
{cause.title().to_string()}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
mod assets;
|
mod assets;
|
||||||
mod class;
|
mod class;
|
||||||
mod clients;
|
mod clients;
|
||||||
|
mod scroll;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod test_util;
|
mod test_util;
|
||||||
mod components {
|
mod components {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
use yew::WheelEvent;
|
||||||
|
|
||||||
|
pub fn vertical_scroll_to_horizontal(selector: &str) -> impl Fn(WheelEvent) {
|
||||||
|
let selector = selector.to_string();
|
||||||
|
move |ev: WheelEvent| {
|
||||||
|
let scroll_y = ev.delta_y();
|
||||||
|
if scroll_y == 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(target) = gloo::utils::document()
|
||||||
|
.query_selector(&selector)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
target.set_scroll_left(target.scroll_left() + ((scroll_y + ev.delta_x()) as i32));
|
||||||
|
ev.prevent_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue