diff --git a/messages/en.json b/messages/en.json index c58a760d..b0926b95 100644 --- a/messages/en.json +++ b/messages/en.json @@ -734,9 +734,11 @@ "night_death_angel": "{1:@} was attacked last night, but luckily, the {=guardian angel!role} was on duty.", "night_death_bodyguard": "{2:@} sacrificed their life to guard that of another.", "lycan_turn": "HOOOOOOOOOWL. You have become... a {=wolf!role}!", - "totem_banish": "{0:@}'s totem emitted a brilliant flash of light last night. It appears that {1:@}'s spirit was driven away by the flash.", - "totem_death": "{0:@}'s totem emitted a brilliant flash of light last night. The dead body of {1:@}, {2!role:article} {2!role:bold}, was found at the scene.", - "totem_death_no_reveal": "{0:@}'s totem emitted a brilliant flash of light last night. The dead body of {1:@} was found at the scene.", + "retribution_totem_night_banish": "{0:@}'s totem emitted a brilliant flash of light last night. It appears that {1:@}'s spirit was driven away by the flash.", + "retribution_totem_night_death": "{0:@}'s totem emitted a brilliant flash of light last night. The dead body of {1:@}, {2!role:article} {2!role:bold}, was found at the scene.", + "retribution_totem_night_death_no_reveal": "{0:@}'s totem emitted a brilliant flash of light last night. The dead body of {1:@} was found at the scene.", + "retribution_totem_day_death": "{0:@}'s totem emits a brilliant flash of light. When the villagers are able to see again, they discover that {1:@}, {2!role:article} {2!role:bold}, has fallen over dead.", + "retribution_totem_day_death_no_reveal": "{0:@}'s totem emits a brilliant flash of light. When the villagers are able to see again, they discover that {1:@} has fallen over dead.", "death": "The dead body of {0:@}, {1!role:article} {1!role:bold}, is found. Those remaining mourn the tragedy.", "death_no_reveal": "The dead body of {0:@} is found. Those remaining mourn the tragedy.", "visited_victim": "{0:@}, {1!role:article} {1!role:bold}, made the unfortunate mistake of visiting the victim's house last night and is now dead.", @@ -995,7 +997,7 @@ "lycanthropy_totem": "If the player who is given the lycanthropy totem is targeted by wolves tomorrow night, they will become a wolf.", "luck_totem": "If the player who is given the luck totem is targeted tomorrow night, one of the players adjacent to them will be targeted instead.", "pestilence_totem": "If the player who is given the pestilence totem is killed by wolves tomorrow night, the wolves will not be able to kill the night after.", - "retribution_totem": "If the player who is given the retribution totem will die tonight, they also kill anyone who killed them.", + "retribution_totem": "If the player who is given the retribution totem is killed tonight or tomorrow by anything except a village vote, they also kill one of the people who killed them.", "misdirection_totem": "If the player who is given the misdirection totem attempts to use a power the following day or night, they will target a player adjacent to their intended target instead of the player they targeted.", "deceit_totem": "If the player who is given the deceit totem is a seer or an oracle, or is seen by a seer or an oracle, the vision will be shifted: if the person would be seen as wolf, they are instead seen as a villager; otherwise, they are seen as a wolf.", "hunter_notify": "You are {=hunter!role:article} {=hunter!role:bold}. Once per game, you may kill another player with \"{=kill!command} \". If you do not wish to kill anyone tonight, use \"{=pass!command}\" instead.", @@ -1270,6 +1272,7 @@ "cat_land": "The cat lands on its [b]feet[/b].", "vengeful_role": "You are {=vengeful ghost!role:article} {=vengeful ghost!role:bold} who is against the {0!role:bold:plural}.", "show_role": "You are {0!role:article} {0!role:bold}.", + "show_secondary_roles": "You are also the following secondary roles: {0:join(!role:bold)}.", "original_wolves": "Original wolves: {0:join}", "assassin_targeting": "You are {=assassin!role:article} {=assassin!role:bold} and targeting {0}.", "assassin_no_target": "You are {=assassin!role:article} {=assassin!role:bold} and do not currently have a target.", diff --git a/src/roles/cursed.py b/src/roles/cursed.py index 6344b3dc..f34de8cb 100644 --- a/src/roles/cursed.py +++ b/src/roles/cursed.py @@ -5,7 +5,7 @@ from src.functions import get_all_players, get_main_role from src.gamestate import GameState from src.messages import messages - +from src.users import User @event_listener("see") def on_see(evt: Event, var: GameState, seer, target): @@ -24,3 +24,9 @@ def on_send_role(evt: Event, var: GameState): for player in cursed: if get_main_role(var, player) == "cursed villager" or player in wolves: player.send(messages["cursed_notify"]) + +@event_listener("myrole") +def on_myrole(evt: Event, var: GameState, player: User): + wolves = get_all_players(var, Wolfchat) + if player not in wolves: + evt.data["secondary"].discard("cursed villager") diff --git a/src/roles/fallenangel.py b/src/roles/fallenangel.py index 5522d997..6f8951cb 100644 --- a/src/roles/fallenangel.py +++ b/src/roles/fallenangel.py @@ -4,7 +4,7 @@ from src import status from src.events import Event, event_listener -from src.functions import get_all_players +from src.functions import get_players, get_all_roles from src.gamestate import GameState from src.roles.helper.wolves import register_wolf from src.users import User @@ -13,7 +13,11 @@ @event_listener("try_protection") def on_try_protection(evt: Event, var: GameState, target: User, attacker: User, attacker_role: str, reason: str): - if attacker_role == "wolf" and get_all_players(var, ("fallen angel",)): + # main role FAs punch through protections for shared wolf kills, + # secondary FAs only punch through protections for their own kills + main_fas = get_players(var, ("fallen angel",)) + all_roles = get_all_roles(var, attacker) if attacker is not None else set() + if (attacker_role == "wolf" and main_fas) or "fallen angel" in all_roles: status.remove_all_protections(var, target, attacker=attacker, attacker_role="fallen angel", reason="fallen_angel") evt.prevent_default = True diff --git a/src/roles/helper/shamans.py b/src/roles/helper/shamans.py index 6f77e2c5..8d219272 100644 --- a/src/roles/helper/shamans.py +++ b/src/roles/helper/shamans.py @@ -14,7 +14,7 @@ match_totem) from src.gamestate import GameState from src.messages import messages -from src.status import try_misdirection, try_protection, try_exchange, is_dying, add_dying +from src.status import try_misdirection, try_protection, try_exchange, add_dying from src.dispatcher import MessageDispatcher from src.users import User from src.locations import move_player_home @@ -472,11 +472,12 @@ def on_del_player(evt: Event, var: GameState, player: User, all_roles: set[str], if not death_triggers or player not in RETRIBUTION: return loser = evt.params.killer + all_players = get_players(var) if loser is None and evt.params.killer_role == "wolf" and evt.params.reason == "night_kill": pl = get_players(var, Wolf & Killer) if pl: loser = random.choice(pl) - if loser is None or is_dying(var, loser): + if loser is None or loser not in all_players: # person that killed us is already dead? return @@ -484,7 +485,8 @@ def on_del_player(evt: Event, var: GameState, player: User, all_roles: set[str], ret_evt.dispatch(var, player, loser) loser = ret_evt.data["target"] channels.Main.send(*ret_evt.data["message"]) - if loser is not None and is_dying(var, loser): + if loser not in all_players: + # another check for the person already being dead since it may have changed via the event loser = None if loser is not None: protected = try_protection(var, loser, player, evt.params.main_role, "retribution_totem") @@ -492,9 +494,11 @@ def on_del_player(evt: Event, var: GameState, player: User, all_roles: set[str], channels.Main.send(*protected) return - to_send = "totem_death_no_reveal" + # message keys: retribution_totem_night_death, retribution_totem_day_death, + # retribution_totem_night_death_no_reveal, retribution_totem_day_death_no_reveal + to_send = f"retribution_totem_{var.current_phase}_death_no_reveal" if var.role_reveal in ("on", "team"): - to_send = "totem_death" + to_send = f"retribution_totem_{var.current_phase}_death" channels.Main.send(messages[to_send].format(player, loser, get_reveal_role(var, loser))) add_dying(var, loser, evt.params.main_role, "retribution_totem", killer=player) diff --git a/src/roles/vengefulghost.py b/src/roles/vengefulghost.py index 61f62ef8..f0539c57 100644 --- a/src/roles/vengefulghost.py +++ b/src/roles/vengefulghost.py @@ -152,7 +152,8 @@ def on_retribution_kill(evt: Event, var: GameState, victim: User, orig_target: U if target in GHOSTS: drivenoff[target] = GHOSTS[target] GHOSTS[target] = "!" + GHOSTS[target] - evt.data["message"].append(messages["totem_banish"].format(victim, target)) + # VGs only kill at night so we only need a night message + evt.data["message"].append(messages["retribution_totem_night_banish"].format(victim, target)) evt.data["target"] = None @event_listener("get_participant_role") diff --git a/src/roles/wildchild.py b/src/roles/wildchild.py index 2526d6a0..d84a0639 100644 --- a/src/roles/wildchild.py +++ b/src/roles/wildchild.py @@ -69,8 +69,11 @@ def on_swap_role_state(evt: Event, var: GameState, actor: User, target: User, ro @event_listener("myrole") def on_myrole(evt: Event, var: GameState, user: User): - if user in IDOLS and user not in get_players(var, get_wolfchat_roles()): - evt.data["messages"].append(messages["wild_child_idol"].format(IDOLS[user])) + if user in IDOLS: + if user not in get_players(var, get_wolfchat_roles()): + evt.data["messages"].append(messages["wild_child_idol"].format(IDOLS[user])) + else: + evt.data["secondary"].discard("wild child") @event_listener("del_player") def on_del_player(evt: Event, var: GameState, player: User, all_roles: set[str], death_triggers: bool): diff --git a/src/wolfgame.py b/src/wolfgame.py index 037fe07d..87686574 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -59,7 +59,7 @@ from src.functions import ( get_players, get_all_players, get_participants, - get_main_role, get_reveal_role, + get_main_role, get_reveal_role, get_all_roles, match_role, match_mode ) @@ -1061,15 +1061,19 @@ def myrole(wrapper: MessageDispatcher, message: str): return role = get_main_role(var, wrapper.source) + # we want secondary to be a set, not a Category, so any sets need a .roles accessor to get at the underlying roles + secondary = get_all_roles(var, wrapper.source) - {role} - Hidden.roles if role in Hidden: role = var.hidden_role - evt = Event("myrole", {"role": role, "messages": []}) + evt = Event("myrole", {"role": role, "secondary": secondary, "messages": []}) if not evt.dispatch(var, wrapper.source): return role = evt.data["role"] wrapper.pm(messages["show_role"].format(role)) + if secondary: + wrapper.pm(messages["show_secondary_roles"].format(sorted(secondary))) for msg in evt.data["messages"]: wrapper.pm(msg)