if you die tonight you might not get woken up
about footer
This commit is contained in:
parent
ad7ffaac31
commit
f711ca0a55
|
|
@ -306,6 +306,15 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
|
|
@ -2415,7 +2424,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"chrono",
|
||||
"ciborium",
|
||||
"convert_case",
|
||||
"convert_case 0.8.0",
|
||||
"futures",
|
||||
"getrandom 0.3.3",
|
||||
"gloo 0.11.0",
|
||||
|
|
@ -2441,7 +2450,8 @@ dependencies = [
|
|||
name = "werewolves-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"chrono",
|
||||
"convert_case 0.9.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ proc-macro = true
|
|||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2", features = ["full", "extra-traits"] }
|
||||
convert_case = { version = "0.8" }
|
||||
convert_case = { version = "0.9" }
|
||||
chrono = { version = "0.4" }
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use core::error::Error;
|
|||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use convert_case::Casing;
|
||||
|
|
@ -574,3 +575,73 @@ pub fn ref_and_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
|||
let ref_and_mut = parse_macro_input!(input as RefAndMut);
|
||||
quote! {#ref_and_mut}.into()
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn build_dirty(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
if !input.is_empty() {
|
||||
panic!("build_dirty doesn't take arguments");
|
||||
}
|
||||
|
||||
let git_state = Command::new("git")
|
||||
.arg("diff")
|
||||
.arg("--stat")
|
||||
.output()
|
||||
.unwrap();
|
||||
if !git_state.status.success() {
|
||||
panic!("git diff --stat failed");
|
||||
}
|
||||
let dirty = !git_state.stdout.is_empty();
|
||||
|
||||
quote! {#dirty}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn build_id(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
if !input.is_empty() {
|
||||
panic!("build_id doesn't take arguments");
|
||||
}
|
||||
let output = Command::new("git")
|
||||
.arg("rev-parse")
|
||||
.arg("--short")
|
||||
.arg("HEAD")
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("git rev-parse --short HEAD failed");
|
||||
}
|
||||
|
||||
let output = String::from_utf8(output.stdout).unwrap();
|
||||
let git_ref = output.trim();
|
||||
|
||||
quote! {#git_ref}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn build_id_long(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
if !input.is_empty() {
|
||||
panic!("build_id doesn't take arguments");
|
||||
}
|
||||
let output = Command::new("git")
|
||||
.arg("rev-parse")
|
||||
.arg("HEAD")
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("git rev-parse --short HEAD failed");
|
||||
}
|
||||
|
||||
let output = String::from_utf8(output.stdout).unwrap();
|
||||
let git_ref = output.trim();
|
||||
|
||||
quote! {#git_ref}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn build_time(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
if !input.is_empty() {
|
||||
panic!("build_time doesn't take arguments");
|
||||
}
|
||||
|
||||
let time = chrono::Utc::now().format("%d %b %Y %T UTC").to_string();
|
||||
|
||||
quote! {#time}.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ fn resolve_protection(
|
|||
}
|
||||
|
||||
pub fn resolve_kill(
|
||||
changes: &mut ChangesLookup<'_>,
|
||||
changes: &ChangesLookup<'_>,
|
||||
target: CharacterId,
|
||||
died_to: &DiedTo,
|
||||
night: u8,
|
||||
|
|
@ -133,7 +133,7 @@ pub fn resolve_kill(
|
|||
night,
|
||||
starves_if_fails: true,
|
||||
} = died_to
|
||||
&& let Some(protection) = changes.protected_take(target)
|
||||
&& let Some(protection) = changes.protected(target)
|
||||
{
|
||||
return Ok(Some(
|
||||
resolve_protection(*source, died_to, target, &protection, *night).unwrap_or(
|
||||
|
|
@ -149,7 +149,7 @@ pub fn resolve_kill(
|
|||
{
|
||||
let killing_wolf = village.character_by_id(*killing_wolf)?;
|
||||
|
||||
match changes.protected_take(target) {
|
||||
match changes.protected(target) {
|
||||
Some(protection) => {
|
||||
return Ok(resolve_protection(
|
||||
killing_wolf.character_id(),
|
||||
|
|
@ -162,7 +162,7 @@ pub fn resolve_kill(
|
|||
None => {
|
||||
// Wolf kill went through -- can kill shifter
|
||||
return Ok(Some(KillOutcome::Single(
|
||||
*ss_source,
|
||||
ss_source,
|
||||
DiedTo::Shapeshift {
|
||||
into: target,
|
||||
night: *night,
|
||||
|
|
@ -172,7 +172,7 @@ pub fn resolve_kill(
|
|||
};
|
||||
}
|
||||
|
||||
let protection = match changes.protected_take(target) {
|
||||
let protection = match changes.protected(target) {
|
||||
Some(prot) => prot,
|
||||
None => return Ok(Some(KillOutcome::Single(target, died_to.clone()))),
|
||||
};
|
||||
|
|
@ -187,7 +187,7 @@ pub fn resolve_kill(
|
|||
.ok_or(GameError::GuardianInvalidOriginalKill)?,
|
||||
original_target: target,
|
||||
original_kill: died_to.clone(),
|
||||
guardian: source,
|
||||
guardian: *source,
|
||||
night: NonZeroU8::new(night).unwrap(),
|
||||
})),
|
||||
Protection::Guardian {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use crate::{
|
|||
error::GameError,
|
||||
game::{
|
||||
GameTime, Village,
|
||||
kill::{self},
|
||||
kill::{self, KillOutcome},
|
||||
night::changes::{ChangesLookup, NightChange},
|
||||
},
|
||||
message::night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
|
||||
|
|
@ -1019,20 +1019,7 @@ impl Night {
|
|||
fn died_to_tonight(&self, character_id: CharacterId) -> Result<Option<DiedTo>> {
|
||||
let ch = self.current_changes();
|
||||
let mut changes = ChangesLookup::new(&ch);
|
||||
if let Some(died_to) = changes.killed(character_id)
|
||||
&& kill::resolve_kill(
|
||||
&mut changes,
|
||||
character_id,
|
||||
died_to,
|
||||
self.night,
|
||||
&self.village,
|
||||
)?
|
||||
.is_some()
|
||||
{
|
||||
Ok(Some(died_to.clone()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
changes.died_to(character_id, self.night, &self.village)
|
||||
}
|
||||
|
||||
/// returns the matching [Character] with the current night's aura changes
|
||||
|
|
@ -1057,6 +1044,16 @@ impl Night {
|
|||
.iter()
|
||||
.flat_map(|(_, _, act)| act.iter())
|
||||
.cloned()
|
||||
.chain(
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
current_changes, ..
|
||||
} => Some(current_changes.iter().cloned()),
|
||||
NightState::Complete => None,
|
||||
}
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
//
|
||||
// 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::ops::Not;
|
||||
use core::{num::NonZeroU8, ops::Not};
|
||||
|
||||
use super::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -22,7 +22,10 @@ use crate::{
|
|||
aura::Aura,
|
||||
character::CharacterId,
|
||||
diedto::DiedTo,
|
||||
game::{Village, kill},
|
||||
game::{
|
||||
Village,
|
||||
kill::{self, KillOutcome},
|
||||
},
|
||||
player::Protection,
|
||||
role::{RoleBlock, RoleTitle},
|
||||
};
|
||||
|
|
@ -112,20 +115,57 @@ impl<'a> ChangesLookup<'a> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn kill_outcomes(&self, night: u8, village: &Village) -> Result<Box<[KillOutcome]>> {
|
||||
self.0
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
NightChange::Kill { target, died_to } => Some((*target, died_to.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.map(|(kill_target, died_to)| {
|
||||
kill::resolve_kill(self, kill_target, &died_to, night, village)
|
||||
})
|
||||
.filter_map(|result| match result {
|
||||
Ok(Some(outcome)) => Some(Ok(outcome)),
|
||||
Ok(None) => None,
|
||||
Err(err) => Some(Err(err)),
|
||||
})
|
||||
.collect::<Result<Box<[_]>>>()
|
||||
}
|
||||
|
||||
pub fn died_to(
|
||||
&mut self,
|
||||
&self,
|
||||
character_id: CharacterId,
|
||||
night: u8,
|
||||
village: &Village,
|
||||
) -> Result<Option<DiedTo>> {
|
||||
if let Some(died_to) = self.killed(character_id)
|
||||
&& kill::resolve_kill(self, character_id, died_to, night, village)?.is_some()
|
||||
{
|
||||
Ok(Some(died_to.clone()))
|
||||
let kill_outcomes = self.kill_outcomes(night, village)?;
|
||||
|
||||
Ok(kill_outcomes.into_iter().find_map(|outcome| match outcome {
|
||||
KillOutcome::Single(target, died_to) => (target == character_id).then_some(died_to),
|
||||
KillOutcome::Guarding {
|
||||
original_killer,
|
||||
original_target,
|
||||
original_kill,
|
||||
guardian,
|
||||
night,
|
||||
} => {
|
||||
if original_killer == character_id {
|
||||
Some(DiedTo::GuardianProtecting {
|
||||
source: guardian,
|
||||
protecting: original_target,
|
||||
protecting_from: original_killer,
|
||||
protecting_from_cause: Box::new(original_kill),
|
||||
night,
|
||||
})
|
||||
} else if guardian == character_id {
|
||||
Some(original_kill)
|
||||
} else {
|
||||
Ok(None)
|
||||
None
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
|
|
@ -160,7 +200,7 @@ impl<'a> ChangesLookup<'a> {
|
|||
None
|
||||
}
|
||||
}
|
||||
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
|
||||
pub fn protected(&self, target: CharacterId) -> Option<&'a Protection> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
.contains(&idx)
|
||||
|
|
@ -169,20 +209,27 @@ impl<'a> ChangesLookup<'a> {
|
|||
NightChange::Protection {
|
||||
target: t,
|
||||
protection,
|
||||
} => (t == target).then_some(protection),
|
||||
} => (*t == target).then_some(protection),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shapeshifter(&self) -> Option<&'a CharacterId> {
|
||||
pub fn shapeshifter(&self) -> Option<CharacterId> {
|
||||
self.shapeshift_change().and_then(|c| match c {
|
||||
NightChange::Shapeshift { source, .. } => Some(source),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shapeshift_change(&self) -> Option<NightChange> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
.contains(&idx)
|
||||
.not()
|
||||
.then_some(match c {
|
||||
NightChange::Shapeshift { source, .. } => Some(source),
|
||||
NightChange::Shapeshift { .. } => Some(c.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use crate::{
|
|||
error::GameError,
|
||||
game::night::{CurrentResult, Night, NightState, changes::NightChange},
|
||||
message::night::{ActionPrompt, ActionResult},
|
||||
role::RoleBlock,
|
||||
role::{RoleBlock, RoleTitle},
|
||||
};
|
||||
|
||||
use super::Result;
|
||||
|
|
@ -82,7 +82,7 @@ impl Night {
|
|||
} => return Err(GameError::AwaitingResponse),
|
||||
NightState::Complete => return Err(GameError::NightOver),
|
||||
}
|
||||
if let Some(prompt) = self.action_queue.pop_front() {
|
||||
if let Some(prompt) = self.pull_next_prompt_with_dead_ignore()? {
|
||||
if let ActionPrompt::Insomniac { character_id } = &prompt
|
||||
&& self.get_visits_for(character_id.character_id).is_empty()
|
||||
{
|
||||
|
|
@ -188,4 +188,25 @@ impl Night {
|
|||
*result = ActionResult::RoleBlocked;
|
||||
});
|
||||
}
|
||||
|
||||
fn pull_next_prompt_with_dead_ignore(&mut self) -> Result<Option<ActionPrompt>> {
|
||||
let has_living_beholder = self
|
||||
.village
|
||||
.characters()
|
||||
.into_iter()
|
||||
.any(|c| matches!(c.role_title(), RoleTitle::Beholder));
|
||||
while let Some(prompt) = self.action_queue.pop_front() {
|
||||
let Some(char_id) = prompt.character_id() else {
|
||||
return Ok(Some(prompt));
|
||||
};
|
||||
match (self.died_to_tonight(char_id)?, has_living_beholder) {
|
||||
(Some(_), false) => {}
|
||||
(Some(DiedTo::Shapeshift { .. }), _) | (Some(_), true) | (None, _) => {
|
||||
return Ok(Some(prompt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ impl Village {
|
|||
GameTime::Day { .. } => return Err(GameError::NotNight),
|
||||
GameTime::Night { number } => number,
|
||||
};
|
||||
let mut changes = ChangesLookup::new(all_changes);
|
||||
let changes = ChangesLookup::new(all_changes);
|
||||
|
||||
let mut new_village = self.clone();
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ impl Village {
|
|||
.died_to(hunter_character.character_id(), night, self)?
|
||||
.is_some()
|
||||
&& let Some(kill) = kill::resolve_kill(
|
||||
&mut changes,
|
||||
&changes,
|
||||
*target,
|
||||
&DiedTo::Hunter {
|
||||
killer: *source,
|
||||
|
|
@ -108,8 +108,7 @@ impl Village {
|
|||
}
|
||||
}
|
||||
NightChange::Kill { target, died_to } => {
|
||||
if let Some(kill) =
|
||||
kill::resolve_kill(&mut changes, *target, died_to, night, self)?
|
||||
if let Some(kill) = kill::resolve_kill(&changes, *target, died_to, night, self)?
|
||||
{
|
||||
if let KillOutcome::Guarding {
|
||||
guardian,
|
||||
|
|
@ -135,7 +134,7 @@ impl Village {
|
|||
}
|
||||
NightChange::Shapeshift { source, into } => {
|
||||
if let Some(target) = changes.wolf_pack_kill_target()
|
||||
&& changes.protected(target).is_none()
|
||||
&& changes.protected(*target).is_none()
|
||||
{
|
||||
if *target != *into {
|
||||
log::error!("shapeshift into({into}) != target({target})");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,311 @@
|
|||
// Copyright (C) 2025 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::num::NonZeroU8;
|
||||
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{
|
||||
Game, GameSettings, SetupRole,
|
||||
night::changes::{ChangesLookup, NightChange},
|
||||
},
|
||||
game_test::{GameExt, SettingsExt, gen_players, init_log},
|
||||
player::Protection,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn shapeshift() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let shapeshifter = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Villager, target);
|
||||
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Shapeshift {
|
||||
source: game.character_by_player_id(shapeshifter).character_id(),
|
||||
into: game.character_by_player_id(target).character_id(),
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(shapeshifter).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(Some(DiedTo::Shapeshift {
|
||||
into: game.character_by_player_id(target).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap()
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_protect() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Villager, target);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Protection {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
protection: Protection::Guardian {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
guarding: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(guardian).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_guard() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Villager, target);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Protection {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
protection: Protection::Guardian {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
guarding: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(guardian).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(Some(DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap()
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(wolf).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(Some(DiedTo::GuardianProtecting {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
protecting: game.character_by_player_id(target).character_id(),
|
||||
protecting_from: game.character_by_player_id(wolf).character_id(),
|
||||
protecting_from_cause: Box::new(DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
}),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_protect_from_multiple_attackers() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let militia = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Villager, target);
|
||||
settings.add_and_assign(SetupRole::Villager, militia);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Militia {
|
||||
killer: game.character_by_player_id(militia).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Protection {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
protection: Protection::Guardian {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
guarding: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(guardian).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_protect_someone_else_unprotected_dies() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Seer, target);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Protection {
|
||||
target: game.living_villager().character_id(),
|
||||
protection: Protection::Guardian {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
guarding: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(Some(DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap()
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(guardian).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
//
|
||||
// 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/>.
|
||||
mod changes;
|
||||
mod night_order;
|
||||
mod previous;
|
||||
mod revert;
|
||||
|
|
|
|||
|
|
@ -100,10 +100,6 @@ fn redeemed_scapegoat_role_changes() {
|
|||
game.mark_and_check(seer);
|
||||
game.r#continue().sleep();
|
||||
|
||||
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
|
||||
game.mark_and_check(wolf_char_id);
|
||||
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||
game.r#continue().sleep();
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -820,12 +820,9 @@ clients {
|
|||
}
|
||||
|
||||
.client-nav {
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// top: 0;
|
||||
max-width: 100%;
|
||||
padding: 10px;
|
||||
// background-color: rgba(255, 107, 255, 0.2);
|
||||
height: 37px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: baseline;
|
||||
|
|
@ -957,6 +954,11 @@ input {
|
|||
font-size: 2rem;
|
||||
align-content: stretch;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
background-color: #000;
|
||||
min-width: 2cm;
|
||||
|
||||
& * {
|
||||
margin: 0;
|
||||
|
|
@ -2106,3 +2108,105 @@ li.choice {
|
|||
.story-text {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
|
||||
.dialog {
|
||||
z-index: 5;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.dialog-box {
|
||||
border: 1px solid white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
background-color: black;
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
|
||||
&>button {
|
||||
min-width: 4cm;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline dotted;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.build-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
label {
|
||||
color: hsl(280, 65%, 43%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
max-height: 1cm;
|
||||
justify-content: center;
|
||||
background-color: #000;
|
||||
z-index: 3;
|
||||
padding: 10px 0 10px 0;
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ use crate::{
|
|||
clients::client::connection::{Connection2, ConnectionError},
|
||||
components::{
|
||||
Button, CoverOfDarkness, Identity, Story,
|
||||
client::{ClientNav, Signin},
|
||||
client::{ClientFooter, ClientNav, Signin},
|
||||
},
|
||||
storage::StorageKey,
|
||||
};
|
||||
|
|
@ -253,6 +253,7 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
|||
<>
|
||||
{nav}
|
||||
{content}
|
||||
<ClientFooter />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright (C) 2025 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 yew::prelude::*;
|
||||
|
||||
use crate::components::{Dialog, WithConfirmation};
|
||||
|
||||
const SOURCE_CODE_URL: &str = "https://sectorinf.com/emilis/werewolves";
|
||||
|
||||
#[function_component]
|
||||
pub fn ClientFooter() -> Html {
|
||||
let about_dialog_state = use_state(|| false);
|
||||
let about_dialog = about_dialog_state.then(|| {
|
||||
let cancel_signout = {
|
||||
let dialog = about_dialog_state.clone();
|
||||
Callback::from(move |_| {
|
||||
dialog.set(false);
|
||||
})
|
||||
};
|
||||
let callback = Callback::from(move |_| ());
|
||||
let options: Box<[String]> = Box::new([]);
|
||||
html! {
|
||||
<Dialog
|
||||
options={options}
|
||||
cancel_callback={Some(cancel_signout)}
|
||||
callback={callback}
|
||||
>
|
||||
<About />
|
||||
</Dialog>
|
||||
}
|
||||
});
|
||||
let about_click = {
|
||||
let dialog_set = about_dialog_state.setter();
|
||||
move |_| {
|
||||
dialog_set.set(true);
|
||||
}
|
||||
};
|
||||
|
||||
html! {
|
||||
<nav class="footer">
|
||||
<button class="default-button solid" onclick={about_click}>{"about"}</button>
|
||||
{about_dialog}
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn About() -> Html {
|
||||
let confirm_state = use_state(|| false);
|
||||
let source_code_confirm = {
|
||||
let confirm_callback = {
|
||||
move |_| {
|
||||
let _ = gloo::utils::window().location().set_href(SOURCE_CODE_URL);
|
||||
}
|
||||
};
|
||||
let message = html! {
|
||||
<>
|
||||
<h1>{"this will take you away from the game"}</h1>
|
||||
<h3>{"make sure this isn't an oopsie"}</h3>
|
||||
</>
|
||||
};
|
||||
html! {
|
||||
<WithConfirmation
|
||||
state={confirm_state}
|
||||
confirm_callback={confirm_callback}
|
||||
message={message}
|
||||
>
|
||||
{"source code"}
|
||||
</WithConfirmation>
|
||||
}
|
||||
};
|
||||
let dirty = crate::BUILD_DIRTY.then_some(html! {
|
||||
<>
|
||||
{" "}
|
||||
<span class="dirty">{"(dirty)"}</span>
|
||||
</>
|
||||
});
|
||||
html! {
|
||||
<div class="about">
|
||||
<h1>{"werewolves"}</h1>
|
||||
<div class="build-info">
|
||||
<p class="build-id">
|
||||
<label>{"build: "}</label>
|
||||
<a href={format!("{SOURCE_CODE_URL}/commit/{}", crate::BUILD_ID_LONG)}>
|
||||
{crate::BUILD_ID}
|
||||
{dirty}
|
||||
</a>
|
||||
</p>
|
||||
<p class="build-time">
|
||||
<label>{"built at: "}</label>
|
||||
<span class="time">{crate::BUILD_TIME}</span>
|
||||
</p>
|
||||
</div>
|
||||
<nav class="links">
|
||||
{source_code_confirm}
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,9 @@ pub fn ClientNav(
|
|||
}
|
||||
});
|
||||
|
||||
let number_open = use_state(|| false);
|
||||
let name_open = use_state(|| false);
|
||||
let pronouns_open = use_state(|| false);
|
||||
let number = {
|
||||
let current_value = use_state(String::new);
|
||||
let message_callback = message_callback.clone();
|
||||
|
|
@ -63,8 +66,7 @@ pub fn ClientNav(
|
|||
.number
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| String::from("???"));
|
||||
let open = use_state(|| false);
|
||||
let open_set = open.setter();
|
||||
let open_set = number_open.setter();
|
||||
let on_submit = {
|
||||
let val = current_value.clone();
|
||||
Callback::from(move |_| {
|
||||
|
|
@ -85,19 +87,27 @@ pub fn ClientNav(
|
|||
open_set.set(false);
|
||||
})
|
||||
};
|
||||
let close_others = {
|
||||
let name_open = name_open.clone();
|
||||
let pronouns_open = pronouns_open.clone();
|
||||
Callback::from(move |_| {
|
||||
name_open.set(false);
|
||||
pronouns_open.set(false);
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<ClickableNumberEdit
|
||||
value={current_value.clone()}
|
||||
field_name="number"
|
||||
on_submit={on_submit}
|
||||
state={open}
|
||||
state={number_open.clone()}
|
||||
on_open={close_others}
|
||||
>
|
||||
<div class="number">{current_num}</div>
|
||||
</ClickableNumberEdit>
|
||||
}
|
||||
};
|
||||
let name = {
|
||||
let open = use_state(|| false);
|
||||
let name = use_state(String::new);
|
||||
let on_submit = {
|
||||
let ident = identity.clone();
|
||||
|
|
@ -115,20 +125,29 @@ pub fn ClientNav(
|
|||
})
|
||||
})
|
||||
};
|
||||
let close_others = {
|
||||
let number_open = number_open.clone();
|
||||
let pronouns_open = pronouns_open.clone();
|
||||
Callback::from(move |_| {
|
||||
number_open.set(false);
|
||||
pronouns_open.set(false);
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<ClickableTextEdit
|
||||
value={name.clone()}
|
||||
submit_ident={identity.clone()}
|
||||
field_name="pronouns"
|
||||
on_submit={on_submit}
|
||||
state={open}
|
||||
state={name_open.clone()}
|
||||
on_open={close_others}
|
||||
>
|
||||
<div class="name">{identity.1.name.as_str()}</div>
|
||||
</ClickableTextEdit>
|
||||
}
|
||||
};
|
||||
let pronouns = {
|
||||
let pronouns_state = use_state(String::new);
|
||||
let pronuns_state = use_state(String::new);
|
||||
|
||||
let on_submit = {
|
||||
let ident = identity.clone();
|
||||
|
|
@ -145,14 +164,22 @@ pub fn ClientNav(
|
|||
})
|
||||
})
|
||||
};
|
||||
let open = use_state(|| false);
|
||||
let close_others = {
|
||||
let number_open = number_open.clone();
|
||||
let name_open = name_open.clone();
|
||||
Callback::from(move |_| {
|
||||
number_open.set(false);
|
||||
name_open.set(false);
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<ClickableTextEdit
|
||||
value={pronouns_state}
|
||||
value={pronuns_state}
|
||||
submit_ident={identity.clone()}
|
||||
field_name="pronouns"
|
||||
on_submit={on_submit}
|
||||
state={open}
|
||||
state={pronouns_open}
|
||||
on_open={close_others}
|
||||
>
|
||||
{pronouns}
|
||||
</ClickableTextEdit>
|
||||
|
|
@ -199,6 +226,8 @@ struct ClickableTextEditProps {
|
|||
pub state: UseStateHandle<bool>,
|
||||
#[prop_or(100)]
|
||||
pub max_length: usize,
|
||||
#[prop_or_default]
|
||||
pub on_open: Option<Callback<()>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -211,6 +240,7 @@ fn ClickableTextEdit(
|
|||
on_submit,
|
||||
state,
|
||||
max_length,
|
||||
on_open,
|
||||
}: &ClickableTextEditProps,
|
||||
) -> Html {
|
||||
let on_input = crate::components::input_element_string_oninput(value.setter(), *max_length);
|
||||
|
|
@ -237,7 +267,7 @@ fn ClickableTextEdit(
|
|||
</div>
|
||||
};
|
||||
html! {
|
||||
<ClickableField options={options} state={state.clone()}>
|
||||
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
||||
{children.clone()}
|
||||
</ClickableField>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (C) 2025 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 web_sys::Element;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::Button;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct DialogProps {
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
pub options: Box<[String]>,
|
||||
#[prop_or_default]
|
||||
pub cancel_callback: Option<Callback<()>>,
|
||||
pub callback: Callback<String>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Dialog(
|
||||
DialogProps {
|
||||
children,
|
||||
options,
|
||||
cancel_callback,
|
||||
callback,
|
||||
}: &DialogProps,
|
||||
) -> Html {
|
||||
let options = options
|
||||
.iter()
|
||||
.map(|opt| {
|
||||
let callback = callback.clone();
|
||||
let option = opt.clone();
|
||||
let cb = Callback::from(move |_| {
|
||||
callback.emit(option.clone());
|
||||
});
|
||||
html! {
|
||||
<Button on_click={cb}>{opt.clone()}</Button>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let backdrop_click = cancel_callback.clone().map(|cancel_callback| {
|
||||
Callback::from(move |ev: MouseEvent| {
|
||||
if let Some(div) = ev.target_dyn_into::<Element>()
|
||||
&& div.class_name() == "dialog"
|
||||
{
|
||||
ev.stop_propagation();
|
||||
cancel_callback.emit(());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class="click-backdrop" onclick={backdrop_click}>
|
||||
<div class="dialog">
|
||||
<div class="dialog-box">
|
||||
<div class="message">
|
||||
{children.clone()}
|
||||
</div>
|
||||
<div class="options">
|
||||
{options}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct WithConfirmationProps {
|
||||
pub state: UseStateHandle<bool>,
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
pub confirm_callback: Callback<()>,
|
||||
pub message: Html,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn WithConfirmation(
|
||||
WithConfirmationProps {
|
||||
state,
|
||||
children,
|
||||
confirm_callback,
|
||||
message,
|
||||
}: &WithConfirmationProps,
|
||||
) -> Html {
|
||||
let about_dialog_state = state.clone();
|
||||
let confirmation_dialog = about_dialog_state.then(|| {
|
||||
let cancel_signout = {
|
||||
let dialog = about_dialog_state.clone();
|
||||
Callback::from(move |_| {
|
||||
dialog.set(false);
|
||||
})
|
||||
};
|
||||
let confirm_callback = confirm_callback.clone();
|
||||
let callback = {
|
||||
let dialog = about_dialog_state.clone();
|
||||
Callback::from(move |opt: String| {
|
||||
if opt == "ok" {
|
||||
confirm_callback.emit(());
|
||||
} else {
|
||||
dialog.set(false);
|
||||
}
|
||||
})
|
||||
};
|
||||
let options: Box<[String]> = Box::new([String::from("ok"), String::from("take me back")]);
|
||||
html! {
|
||||
<Dialog
|
||||
options={options}
|
||||
cancel_callback={Some(cancel_signout)}
|
||||
callback={callback}
|
||||
>
|
||||
{message.clone()}
|
||||
</Dialog>
|
||||
}
|
||||
});
|
||||
let confirmation_click = {
|
||||
let dialog_set = about_dialog_state.setter();
|
||||
move |_| {
|
||||
dialog_set.set(true);
|
||||
}
|
||||
};
|
||||
html! {
|
||||
<>
|
||||
<Button on_click={confirmation_click}>{children.clone()}</Button>
|
||||
{confirmation_dialog}
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ pub struct ClickableFieldProps {
|
|||
#[prop_or_default]
|
||||
pub with_backdrop_exit: bool,
|
||||
pub state: UseStateHandle<bool>,
|
||||
#[prop_or_default]
|
||||
pub on_open: Option<Callback<()>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -39,11 +41,20 @@ pub fn ClickableField(
|
|||
button_class,
|
||||
with_backdrop_exit,
|
||||
state,
|
||||
on_open,
|
||||
}: &ClickableFieldProps,
|
||||
) -> Html {
|
||||
let open = state.clone();
|
||||
let on_click_open = open.clone();
|
||||
let open_close = Callback::from(move |_| on_click_open.set(!(*on_click_open)));
|
||||
let open_close = {
|
||||
let open = open.clone();
|
||||
let on_open = on_open.clone();
|
||||
Callback::from(move |_| {
|
||||
if !*open && let Some(on_open) = on_open.as_ref() {
|
||||
on_open.emit(());
|
||||
}
|
||||
open.set(!(*open));
|
||||
})
|
||||
};
|
||||
let submenu_open_close = open_close.clone();
|
||||
let submenu = open.clone().then(|| {
|
||||
let backdrop = with_backdrop_exit.then(|| {
|
||||
|
|
@ -79,6 +90,8 @@ pub struct ClickableNumberEditProps {
|
|||
pub on_submit: Callback<()>,
|
||||
pub field_name: &'static str,
|
||||
pub state: UseStateHandle<bool>,
|
||||
#[prop_or_default]
|
||||
pub on_open: Option<Callback<()>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -89,6 +102,7 @@ pub fn ClickableNumberEdit(
|
|||
field_name,
|
||||
on_submit,
|
||||
state,
|
||||
on_open,
|
||||
}: &ClickableNumberEditProps,
|
||||
) -> Html {
|
||||
let on_input = crate::components::input_element_string_oninput(value.setter(), 20);
|
||||
|
|
@ -101,7 +115,7 @@ pub fn ClickableNumberEdit(
|
|||
</div>
|
||||
};
|
||||
html! {
|
||||
<ClickableField options={options} state={state.clone()}>
|
||||
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
||||
{children.clone()}
|
||||
</ClickableField>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ use pages::{ErrorComponent, WerewolfError};
|
|||
use web_sys::Url;
|
||||
use yew::{context::ContextProviderProps, prelude::*};
|
||||
|
||||
const BUILD_ID: &str = werewolves_macros::build_id!();
|
||||
const BUILD_ID_LONG: &str = werewolves_macros::build_id_long!();
|
||||
const BUILD_DIRTY: bool = werewolves_macros::build_dirty!();
|
||||
const BUILD_TIME: &str = werewolves_macros::build_time!();
|
||||
|
||||
use crate::clients::{
|
||||
client::{Client2, ClientContext},
|
||||
host::{Host, HostEvent},
|
||||
|
|
@ -47,6 +52,7 @@ use crate::clients::{
|
|||
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
log::debug!("starting werewolves build {BUILD_ID}");
|
||||
let document = gloo::utils::document();
|
||||
let url = document.document_uri().expect("get uri");
|
||||
let url_obj = Url::new(&url).unwrap();
|
||||
|
|
|
|||
Loading…
Reference in New Issue