diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs
index 2b10931..a35365b 100644
--- a/werewolves-proto/src/character.rs
+++ b/werewolves-proto/src/character.rs
@@ -552,7 +552,7 @@ impl Character {
&& village
.executions_on_day(last_day)
.iter()
- .any(|c| c.is_village())
+ .any(|c| c.alignment().village())
{
prompts.push(ActionPrompt::Vindicator {
character_id: self.identity(),
diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs
index 007f6d0..d163a54 100644
--- a/werewolves-proto/src/game_test/mod.rs
+++ b/werewolves-proto/src/game_test/mod.rs
@@ -934,6 +934,10 @@ fn big_game_test_based_on_story_test() {
game.mark(protect.character_id());
game.r#continue().sleep();
+ game.next().title().vindicator();
+ game.mark_villager();
+ game.r#continue().sleep();
+
game.next().title().wolf_pack_kill();
game.mark(protect.character_id());
game.r#continue().r#continue();
diff --git a/werewolves-proto/src/game_test/previous.rs b/werewolves-proto/src/game_test/previous.rs
index 9fb4e05..9fa8b9f 100644
--- a/werewolves-proto/src/game_test/previous.rs
+++ b/werewolves-proto/src/game_test/previous.rs
@@ -348,6 +348,10 @@ fn previous_prompt() {
game.mark(protect.character_id());
game.r#continue().sleep();
+ game.next().title().vindicator();
+ game.mark_villager();
+ game.r#continue().sleep();
+
game.next().title().wolf_pack_kill();
game.mark(protect.character_id());
game.r#continue().r#continue();
diff --git a/werewolves-proto/src/game_test/role/mod.rs b/werewolves-proto/src/game_test/role/mod.rs
index 1f33ccb..82e3772 100644
--- a/werewolves-proto/src/game_test/role/mod.rs
+++ b/werewolves-proto/src/game_test/role/mod.rs
@@ -32,4 +32,5 @@ mod protector;
mod pyremaster;
mod scapegoat;
mod shapeshifter;
+mod vindicator;
mod weightlifter;
diff --git a/werewolves-proto/src/game_test/role/vindicator.rs b/werewolves-proto/src/game_test/role/vindicator.rs
new file mode 100644
index 0000000..c330514
--- /dev/null
+++ b/werewolves-proto/src/game_test/role/vindicator.rs
@@ -0,0 +1,109 @@
+// 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 .
+use core::num::NonZeroU8;
+
+#[allow(unused)]
+use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
+
+use crate::{
+ diedto::DiedTo,
+ game::{Game, GameSettings, SetupRole},
+ game_test::{
+ ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
+ },
+ message::night::ActionPromptTitle,
+};
+
+#[test]
+fn direwolf_kill_activates() {
+ init_log();
+ let players = gen_players(1..21);
+ let mut player_ids = players.iter().map(|p| p.player_id);
+ let vindicator = player_ids.next().unwrap();
+ let wolf = player_ids.next().unwrap();
+ let direwolf = player_ids.next().unwrap();
+ let mut settings = GameSettings::empty();
+ settings.add_and_assign(SetupRole::Werewolf, wolf);
+ settings.add_and_assign(SetupRole::DireWolf, direwolf);
+ settings.add_and_assign(SetupRole::Vindicator, vindicator);
+ settings.fill_remaining_slots_with_villagers(players.len());
+ let mut game = Game::new(&players, settings).unwrap();
+ game.r#continue().r#continue();
+ assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
+ game.r#continue().r#continue();
+
+ game.next().title().direwolf();
+ game.mark_villager();
+ game.r#continue().sleep();
+
+ game.next_expect_day();
+ game.mark_for_execution(game.character_by_player_id(direwolf).character_id());
+ game.execute().title().vindicator();
+ let prot = game.living_villager();
+ game.mark(prot.character_id());
+ game.r#continue().sleep();
+
+ game.next().title().wolf_pack_kill();
+ game.mark(prot.character_id());
+ game.r#continue().sleep();
+
+ game.next_expect_day();
+
+ assert_eq!(
+ game.character_by_player_id(prot.player_id())
+ .died_to()
+ .cloned(),
+ None
+ );
+}
+
+#[test]
+fn maplewolf_kill_does_not_activate() {
+ init_log();
+ let players = gen_players(1..21);
+ let mut player_ids = players.iter().map(|p| p.player_id);
+ let vindicator = player_ids.next().unwrap();
+ let wolf = player_ids.next().unwrap();
+ let maplewolf = player_ids.next().unwrap();
+ let mut settings = GameSettings::empty();
+ settings.add_and_assign(SetupRole::Werewolf, wolf);
+ settings.add_and_assign(SetupRole::MapleWolf, maplewolf);
+ settings.add_and_assign(SetupRole::Vindicator, vindicator);
+ settings.fill_remaining_slots_with_villagers(players.len());
+ let mut game = Game::new(&players, settings).unwrap();
+ game.r#continue().r#continue();
+ assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
+ game.r#continue().sleep();
+
+ game.next_expect_day();
+ game.mark_for_execution(game.character_by_player_id(maplewolf).character_id());
+
+ game.execute().title().wolf_pack_kill();
+ let target = game.living_villager();
+ game.mark(target.character_id());
+ game.r#continue().sleep();
+
+ game.next_expect_day();
+
+ assert_eq!(
+ game.character_by_player_id(target.player_id())
+ .died_to()
+ .cloned(),
+ Some(DiedTo::Wolfpack {
+ killing_wolf: game.character_by_player_id(wolf).character_id(),
+ night: NonZeroU8::new(1).unwrap()
+ })
+ );
+}
diff --git a/werewolves/src/clients/host/host.rs b/werewolves/src/clients/host/host.rs
index daf855b..49bc8eb 100644
--- a/werewolves/src/clients/host/host.rs
+++ b/werewolves/src/clients/host/host.rs
@@ -297,7 +297,7 @@ impl Component for Host {
type Properties = ();
fn create(ctx: &Context) -> Self {
- gloo::utils::document().set_title("Werewolves Host");
+ gloo::utils::document().set_title(format!("{} — host", crate::TITLE).as_str());
if let Some(clients) = gloo::utils::document()
.query_selector("clients")
.ok()
diff --git a/werewolves/src/main.rs b/werewolves/src/main.rs
index 64e3705..15b91a8 100644
--- a/werewolves/src/main.rs
+++ b/werewolves/src/main.rs
@@ -45,6 +45,10 @@ 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!();
+const TITLE: &str = match option_env!("LOCAL") {
+ Some(_) => "LOCAL werewolves",
+ None => "werewolves",
+};
use crate::clients::{
client::{Client2, ClientContext},
@@ -55,6 +59,7 @@ fn main() {
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
log::debug!("starting werewolves build {BUILD_ID}");
let document = gloo::utils::document();
+ document.set_title(crate::TITLE);
let url = document.document_uri().expect("get uri");
let url_obj = Url::new(&url).unwrap();
let path = url_obj.pathname();