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",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
|
|
@ -2415,7 +2424,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"ciborium",
|
"ciborium",
|
||||||
"convert_case",
|
"convert_case 0.8.0",
|
||||||
"futures",
|
"futures",
|
||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"gloo 0.11.0",
|
"gloo 0.11.0",
|
||||||
|
|
@ -2441,7 +2450,8 @@ dependencies = [
|
||||||
name = "werewolves-macros"
|
name = "werewolves-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"chrono",
|
||||||
|
"convert_case 0.9.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ proc-macro = true
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
syn = { version = "2", features = ["full", "extra-traits"] }
|
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::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use convert_case::Casing;
|
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);
|
let ref_and_mut = parse_macro_input!(input as RefAndMut);
|
||||||
quote! {#ref_and_mut}.into()
|
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(
|
pub fn resolve_kill(
|
||||||
changes: &mut ChangesLookup<'_>,
|
changes: &ChangesLookup<'_>,
|
||||||
target: CharacterId,
|
target: CharacterId,
|
||||||
died_to: &DiedTo,
|
died_to: &DiedTo,
|
||||||
night: u8,
|
night: u8,
|
||||||
|
|
@ -133,7 +133,7 @@ pub fn resolve_kill(
|
||||||
night,
|
night,
|
||||||
starves_if_fails: true,
|
starves_if_fails: true,
|
||||||
} = died_to
|
} = died_to
|
||||||
&& let Some(protection) = changes.protected_take(target)
|
&& let Some(protection) = changes.protected(target)
|
||||||
{
|
{
|
||||||
return Ok(Some(
|
return Ok(Some(
|
||||||
resolve_protection(*source, died_to, target, &protection, *night).unwrap_or(
|
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)?;
|
let killing_wolf = village.character_by_id(*killing_wolf)?;
|
||||||
|
|
||||||
match changes.protected_take(target) {
|
match changes.protected(target) {
|
||||||
Some(protection) => {
|
Some(protection) => {
|
||||||
return Ok(resolve_protection(
|
return Ok(resolve_protection(
|
||||||
killing_wolf.character_id(),
|
killing_wolf.character_id(),
|
||||||
|
|
@ -162,7 +162,7 @@ pub fn resolve_kill(
|
||||||
None => {
|
None => {
|
||||||
// Wolf kill went through -- can kill shifter
|
// Wolf kill went through -- can kill shifter
|
||||||
return Ok(Some(KillOutcome::Single(
|
return Ok(Some(KillOutcome::Single(
|
||||||
*ss_source,
|
ss_source,
|
||||||
DiedTo::Shapeshift {
|
DiedTo::Shapeshift {
|
||||||
into: target,
|
into: target,
|
||||||
night: *night,
|
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,
|
Some(prot) => prot,
|
||||||
None => return Ok(Some(KillOutcome::Single(target, died_to.clone()))),
|
None => return Ok(Some(KillOutcome::Single(target, died_to.clone()))),
|
||||||
};
|
};
|
||||||
|
|
@ -187,7 +187,7 @@ pub fn resolve_kill(
|
||||||
.ok_or(GameError::GuardianInvalidOriginalKill)?,
|
.ok_or(GameError::GuardianInvalidOriginalKill)?,
|
||||||
original_target: target,
|
original_target: target,
|
||||||
original_kill: died_to.clone(),
|
original_kill: died_to.clone(),
|
||||||
guardian: source,
|
guardian: *source,
|
||||||
night: NonZeroU8::new(night).unwrap(),
|
night: NonZeroU8::new(night).unwrap(),
|
||||||
})),
|
})),
|
||||||
Protection::Guardian {
|
Protection::Guardian {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ use crate::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
GameTime, Village,
|
GameTime, Village,
|
||||||
kill::{self},
|
kill::{self, KillOutcome},
|
||||||
night::changes::{ChangesLookup, NightChange},
|
night::changes::{ChangesLookup, NightChange},
|
||||||
},
|
},
|
||||||
message::night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
|
message::night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
|
||||||
|
|
@ -1019,20 +1019,7 @@ impl Night {
|
||||||
fn died_to_tonight(&self, character_id: CharacterId) -> Result<Option<DiedTo>> {
|
fn died_to_tonight(&self, character_id: CharacterId) -> Result<Option<DiedTo>> {
|
||||||
let ch = self.current_changes();
|
let ch = self.current_changes();
|
||||||
let mut changes = ChangesLookup::new(&ch);
|
let mut changes = ChangesLookup::new(&ch);
|
||||||
if let Some(died_to) = changes.killed(character_id)
|
changes.died_to(character_id, self.night, &self.village)
|
||||||
&& kill::resolve_kill(
|
|
||||||
&mut changes,
|
|
||||||
character_id,
|
|
||||||
died_to,
|
|
||||||
self.night,
|
|
||||||
&self.village,
|
|
||||||
)?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
Ok(Some(died_to.clone()))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns the matching [Character] with the current night's aura changes
|
/// returns the matching [Character] with the current night's aura changes
|
||||||
|
|
@ -1057,6 +1044,16 @@ impl Night {
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|(_, _, act)| act.iter())
|
.flat_map(|(_, _, act)| act.iter())
|
||||||
.cloned()
|
.cloned()
|
||||||
|
.chain(
|
||||||
|
match &self.night_state {
|
||||||
|
NightState::Active {
|
||||||
|
current_changes, ..
|
||||||
|
} => Some(current_changes.iter().cloned()),
|
||||||
|
NightState::Complete => None,
|
||||||
|
}
|
||||||
|
.into_iter()
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +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::ops::Not;
|
use core::{num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -22,7 +22,10 @@ use crate::{
|
||||||
aura::Aura,
|
aura::Aura,
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{Village, kill},
|
game::{
|
||||||
|
Village,
|
||||||
|
kill::{self, KillOutcome},
|
||||||
|
},
|
||||||
player::Protection,
|
player::Protection,
|
||||||
role::{RoleBlock, RoleTitle},
|
role::{RoleBlock, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
@ -112,20 +115,57 @@ impl<'a> ChangesLookup<'a> {
|
||||||
.collect()
|
.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(
|
pub fn died_to(
|
||||||
&mut self,
|
&self,
|
||||||
character_id: CharacterId,
|
character_id: CharacterId,
|
||||||
night: u8,
|
night: u8,
|
||||||
village: &Village,
|
village: &Village,
|
||||||
) -> Result<Option<DiedTo>> {
|
) -> Result<Option<DiedTo>> {
|
||||||
if let Some(died_to) = self.killed(character_id)
|
let kill_outcomes = self.kill_outcomes(night, village)?;
|
||||||
&& kill::resolve_kill(self, character_id, died_to, night, village)?.is_some()
|
|
||||||
{
|
Ok(kill_outcomes.into_iter().find_map(|outcome| match outcome {
|
||||||
Ok(Some(died_to.clone()))
|
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 {
|
} else {
|
||||||
Ok(None)
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> {
|
pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> {
|
||||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
|
|
@ -160,7 +200,7 @@ impl<'a> ChangesLookup<'a> {
|
||||||
None
|
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.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
self.1
|
self.1
|
||||||
.contains(&idx)
|
.contains(&idx)
|
||||||
|
|
@ -169,20 +209,27 @@ impl<'a> ChangesLookup<'a> {
|
||||||
NightChange::Protection {
|
NightChange::Protection {
|
||||||
target: t,
|
target: t,
|
||||||
protection,
|
protection,
|
||||||
} => (t == target).then_some(protection),
|
} => (*t == target).then_some(protection),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.flatten()
|
.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.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
self.1
|
self.1
|
||||||
.contains(&idx)
|
.contains(&idx)
|
||||||
.not()
|
.not()
|
||||||
.then_some(match c {
|
.then_some(match c {
|
||||||
NightChange::Shapeshift { source, .. } => Some(source),
|
NightChange::Shapeshift { .. } => Some(c.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use crate::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::night::{CurrentResult, Night, NightState, changes::NightChange},
|
game::night::{CurrentResult, Night, NightState, changes::NightChange},
|
||||||
message::night::{ActionPrompt, ActionResult},
|
message::night::{ActionPrompt, ActionResult},
|
||||||
role::RoleBlock,
|
role::{RoleBlock, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
|
|
@ -82,7 +82,7 @@ impl Night {
|
||||||
} => return Err(GameError::AwaitingResponse),
|
} => return Err(GameError::AwaitingResponse),
|
||||||
NightState::Complete => return Err(GameError::NightOver),
|
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
|
if let ActionPrompt::Insomniac { character_id } = &prompt
|
||||||
&& self.get_visits_for(character_id.character_id).is_empty()
|
&& self.get_visits_for(character_id.character_id).is_empty()
|
||||||
{
|
{
|
||||||
|
|
@ -188,4 +188,25 @@ impl Night {
|
||||||
*result = ActionResult::RoleBlocked;
|
*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::Day { .. } => return Err(GameError::NotNight),
|
||||||
GameTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
let mut changes = ChangesLookup::new(all_changes);
|
let changes = ChangesLookup::new(all_changes);
|
||||||
|
|
||||||
let mut new_village = self.clone();
|
let mut new_village = self.clone();
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ impl Village {
|
||||||
.died_to(hunter_character.character_id(), night, self)?
|
.died_to(hunter_character.character_id(), night, self)?
|
||||||
.is_some()
|
.is_some()
|
||||||
&& let Some(kill) = kill::resolve_kill(
|
&& let Some(kill) = kill::resolve_kill(
|
||||||
&mut changes,
|
&changes,
|
||||||
*target,
|
*target,
|
||||||
&DiedTo::Hunter {
|
&DiedTo::Hunter {
|
||||||
killer: *source,
|
killer: *source,
|
||||||
|
|
@ -108,8 +108,7 @@ impl Village {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NightChange::Kill { target, died_to } => {
|
NightChange::Kill { target, died_to } => {
|
||||||
if let Some(kill) =
|
if let Some(kill) = kill::resolve_kill(&changes, *target, died_to, night, self)?
|
||||||
kill::resolve_kill(&mut changes, *target, died_to, night, self)?
|
|
||||||
{
|
{
|
||||||
if let KillOutcome::Guarding {
|
if let KillOutcome::Guarding {
|
||||||
guardian,
|
guardian,
|
||||||
|
|
@ -135,7 +134,7 @@ impl Village {
|
||||||
}
|
}
|
||||||
NightChange::Shapeshift { source, into } => {
|
NightChange::Shapeshift { source, into } => {
|
||||||
if let Some(target) = changes.wolf_pack_kill_target()
|
if let Some(target) = changes.wolf_pack_kill_target()
|
||||||
&& changes.protected(target).is_none()
|
&& changes.protected(*target).is_none()
|
||||||
{
|
{
|
||||||
if *target != *into {
|
if *target != *into {
|
||||||
log::error!("shapeshift into({into}) != target({target})");
|
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
|
// 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/>.
|
||||||
|
mod changes;
|
||||||
mod night_order;
|
mod night_order;
|
||||||
mod previous;
|
mod previous;
|
||||||
mod revert;
|
mod revert;
|
||||||
|
|
|
||||||
|
|
@ -100,10 +100,6 @@ fn redeemed_scapegoat_role_changes() {
|
||||||
game.mark_and_check(seer);
|
game.mark_and_check(seer);
|
||||||
game.r#continue().sleep();
|
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();
|
game.next_expect_day();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -820,12 +820,9 @@ clients {
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-nav {
|
.client-nav {
|
||||||
// position: absolute;
|
|
||||||
// left: 0;
|
|
||||||
// top: 0;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
// background-color: rgba(255, 107, 255, 0.2);
|
height: 37px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: baseline;
|
justify-content: baseline;
|
||||||
|
|
@ -957,6 +954,11 @@ input {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
z-index: 3;
|
||||||
|
background-color: #000;
|
||||||
|
min-width: 2cm;
|
||||||
|
|
||||||
& * {
|
& * {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -2106,3 +2108,105 @@ li.choice {
|
||||||
.story-text {
|
.story-text {
|
||||||
font-size: 1.7em;
|
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},
|
clients::client::connection::{Connection2, ConnectionError},
|
||||||
components::{
|
components::{
|
||||||
Button, CoverOfDarkness, Identity, Story,
|
Button, CoverOfDarkness, Identity, Story,
|
||||||
client::{ClientNav, Signin},
|
client::{ClientFooter, ClientNav, Signin},
|
||||||
},
|
},
|
||||||
storage::StorageKey,
|
storage::StorageKey,
|
||||||
};
|
};
|
||||||
|
|
@ -253,6 +253,7 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
||||||
<>
|
<>
|
||||||
{nav}
|
{nav}
|
||||||
{content}
|
{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 number = {
|
||||||
let current_value = use_state(String::new);
|
let current_value = use_state(String::new);
|
||||||
let message_callback = message_callback.clone();
|
let message_callback = message_callback.clone();
|
||||||
|
|
@ -63,8 +66,7 @@ pub fn ClientNav(
|
||||||
.number
|
.number
|
||||||
.map(|v| v.to_string())
|
.map(|v| v.to_string())
|
||||||
.unwrap_or_else(|| String::from("???"));
|
.unwrap_or_else(|| String::from("???"));
|
||||||
let open = use_state(|| false);
|
let open_set = number_open.setter();
|
||||||
let open_set = open.setter();
|
|
||||||
let on_submit = {
|
let on_submit = {
|
||||||
let val = current_value.clone();
|
let val = current_value.clone();
|
||||||
Callback::from(move |_| {
|
Callback::from(move |_| {
|
||||||
|
|
@ -85,19 +87,27 @@ pub fn ClientNav(
|
||||||
open_set.set(false);
|
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! {
|
html! {
|
||||||
<ClickableNumberEdit
|
<ClickableNumberEdit
|
||||||
value={current_value.clone()}
|
value={current_value.clone()}
|
||||||
field_name="number"
|
field_name="number"
|
||||||
on_submit={on_submit}
|
on_submit={on_submit}
|
||||||
state={open}
|
state={number_open.clone()}
|
||||||
|
on_open={close_others}
|
||||||
>
|
>
|
||||||
<div class="number">{current_num}</div>
|
<div class="number">{current_num}</div>
|
||||||
</ClickableNumberEdit>
|
</ClickableNumberEdit>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let name = {
|
let name = {
|
||||||
let open = use_state(|| false);
|
|
||||||
let name = use_state(String::new);
|
let name = use_state(String::new);
|
||||||
let on_submit = {
|
let on_submit = {
|
||||||
let ident = identity.clone();
|
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! {
|
html! {
|
||||||
<ClickableTextEdit
|
<ClickableTextEdit
|
||||||
value={name.clone()}
|
value={name.clone()}
|
||||||
submit_ident={identity.clone()}
|
submit_ident={identity.clone()}
|
||||||
field_name="pronouns"
|
field_name="pronouns"
|
||||||
on_submit={on_submit}
|
on_submit={on_submit}
|
||||||
state={open}
|
state={name_open.clone()}
|
||||||
|
on_open={close_others}
|
||||||
>
|
>
|
||||||
<div class="name">{identity.1.name.as_str()}</div>
|
<div class="name">{identity.1.name.as_str()}</div>
|
||||||
</ClickableTextEdit>
|
</ClickableTextEdit>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let pronouns = {
|
let pronouns = {
|
||||||
let pronouns_state = use_state(String::new);
|
let pronuns_state = use_state(String::new);
|
||||||
|
|
||||||
let on_submit = {
|
let on_submit = {
|
||||||
let ident = identity.clone();
|
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! {
|
html! {
|
||||||
<ClickableTextEdit
|
<ClickableTextEdit
|
||||||
value={pronouns_state}
|
value={pronuns_state}
|
||||||
submit_ident={identity.clone()}
|
submit_ident={identity.clone()}
|
||||||
field_name="pronouns"
|
field_name="pronouns"
|
||||||
on_submit={on_submit}
|
on_submit={on_submit}
|
||||||
state={open}
|
state={pronouns_open}
|
||||||
|
on_open={close_others}
|
||||||
>
|
>
|
||||||
{pronouns}
|
{pronouns}
|
||||||
</ClickableTextEdit>
|
</ClickableTextEdit>
|
||||||
|
|
@ -199,6 +226,8 @@ struct ClickableTextEditProps {
|
||||||
pub state: UseStateHandle<bool>,
|
pub state: UseStateHandle<bool>,
|
||||||
#[prop_or(100)]
|
#[prop_or(100)]
|
||||||
pub max_length: usize,
|
pub max_length: usize,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_open: Option<Callback<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
@ -211,6 +240,7 @@ fn ClickableTextEdit(
|
||||||
on_submit,
|
on_submit,
|
||||||
state,
|
state,
|
||||||
max_length,
|
max_length,
|
||||||
|
on_open,
|
||||||
}: &ClickableTextEditProps,
|
}: &ClickableTextEditProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let on_input = crate::components::input_element_string_oninput(value.setter(), *max_length);
|
let on_input = crate::components::input_element_string_oninput(value.setter(), *max_length);
|
||||||
|
|
@ -237,7 +267,7 @@ fn ClickableTextEdit(
|
||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<ClickableField options={options} state={state.clone()}>
|
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
||||||
{children.clone()}
|
{children.clone()}
|
||||||
</ClickableField>
|
</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]
|
#[prop_or_default]
|
||||||
pub with_backdrop_exit: bool,
|
pub with_backdrop_exit: bool,
|
||||||
pub state: UseStateHandle<bool>,
|
pub state: UseStateHandle<bool>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_open: Option<Callback<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
@ -39,11 +41,20 @@ pub fn ClickableField(
|
||||||
button_class,
|
button_class,
|
||||||
with_backdrop_exit,
|
with_backdrop_exit,
|
||||||
state,
|
state,
|
||||||
|
on_open,
|
||||||
}: &ClickableFieldProps,
|
}: &ClickableFieldProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let open = state.clone();
|
let open = state.clone();
|
||||||
let on_click_open = open.clone();
|
let open_close = {
|
||||||
let open_close = Callback::from(move |_| on_click_open.set(!(*on_click_open)));
|
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_close = open_close.clone();
|
||||||
let submenu = open.clone().then(|| {
|
let submenu = open.clone().then(|| {
|
||||||
let backdrop = with_backdrop_exit.then(|| {
|
let backdrop = with_backdrop_exit.then(|| {
|
||||||
|
|
@ -79,6 +90,8 @@ pub struct ClickableNumberEditProps {
|
||||||
pub on_submit: Callback<()>,
|
pub on_submit: Callback<()>,
|
||||||
pub field_name: &'static str,
|
pub field_name: &'static str,
|
||||||
pub state: UseStateHandle<bool>,
|
pub state: UseStateHandle<bool>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_open: Option<Callback<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
@ -89,6 +102,7 @@ pub fn ClickableNumberEdit(
|
||||||
field_name,
|
field_name,
|
||||||
on_submit,
|
on_submit,
|
||||||
state,
|
state,
|
||||||
|
on_open,
|
||||||
}: &ClickableNumberEditProps,
|
}: &ClickableNumberEditProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let on_input = crate::components::input_element_string_oninput(value.setter(), 20);
|
let on_input = crate::components::input_element_string_oninput(value.setter(), 20);
|
||||||
|
|
@ -101,7 +115,7 @@ pub fn ClickableNumberEdit(
|
||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<ClickableField options={options} state={state.clone()}>
|
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
||||||
{children.clone()}
|
{children.clone()}
|
||||||
</ClickableField>
|
</ClickableField>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ use pages::{ErrorComponent, WerewolfError};
|
||||||
use web_sys::Url;
|
use web_sys::Url;
|
||||||
use yew::{context::ContextProviderProps, prelude::*};
|
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::{
|
use crate::clients::{
|
||||||
client::{Client2, ClientContext},
|
client::{Client2, ClientContext},
|
||||||
host::{Host, HostEvent},
|
host::{Host, HostEvent},
|
||||||
|
|
@ -47,6 +52,7 @@ use crate::clients::{
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||||
|
log::debug!("starting werewolves build {BUILD_ID}");
|
||||||
let document = gloo::utils::document();
|
let document = gloo::utils::document();
|
||||||
let url = document.document_uri().expect("get uri");
|
let url = document.document_uri().expect("get uri");
|
||||||
let url_obj = Url::new(&url).unwrap();
|
let url_obj = Url::new(&url).unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue