diff --git a/.gitignore b/.gitignore index 29d45fa..dc51cd8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ __pycache__/ botconfig.py /gamemodes.py /messages.json +/roles/*.py # Database files *.sqlite3 diff --git a/roles/__init__.py.example b/roles/__init__.py.example new file mode 100644 index 0000000..3a3f55c --- /dev/null +++ b/roles/__init__.py.example @@ -0,0 +1,20 @@ +# Imports all custom and built-in role definitions +# To implement custom roles, rename this file to __init__.py +import os.path +import glob +import importlib + +# get built-in roles +import src.roles + +path = os.path.dirname(os.path.abspath(__file__)) +search = os.path.join(path, "*.py") + +for f in glob.iglob(search): + f = os.path.basename(f) + n, _ = os.path.splitext(f) + if f == "__init__.py": + continue + importlib.import_module(n, package="roles") + +# vim: set sw=4 expandtab: diff --git a/src/__init__.py b/src/__init__.py index 2ddf3a5..a7e0df7 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -12,12 +12,18 @@ from src import db # These are not required, so failing to import it doesn't matter # The file then imports our game modes # Fall back to importing our game modes if theirs fail +# Do the same with roles try: import gamemodes except ImportError: import src.gamemodes +try: + import roles +except ImportError: + import src.roles + # Handle launch parameters # Argument --debug means start in debug mode diff --git a/src/decorators.py b/src/decorators.py index d71f225..55a93b8 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -10,7 +10,7 @@ from oyoyo.parse import parse_nick import botconfig import src.settings as var from src.utilities import * -from src import logger, errlog +from src import logger, errlog, events from src.messages import messages adminlog = logger.logger("audit.log") @@ -248,4 +248,24 @@ class hook: if not HOOKS[each]: del HOOKS[each] +class event_listener: + def __init__(self, event, priority=5): + self.event = event + self.priority = priority + self.func = None + + def __call__(self, *args, **kwargs): + if self.func is None: + if isinstance(func, event_listener): + func = func.func + self.func = handle_error(func) + events.add_listener(self.event, self.func, self.priority) + self.__doc__ = self.func.__doc__ + return self + else: + return self.func(*args, **kwargs) + + def remove(self): + events.remove_listener(self.event, self.func, self.priority) + # vim: set sw=4 expandtab: diff --git a/src/roles/__init__.py b/src/roles/__init__.py new file mode 100644 index 0000000..db58322 --- /dev/null +++ b/src/roles/__init__.py @@ -0,0 +1,16 @@ +# Imports all role definitions +import os.path +import glob +import importlib + +path = os.path.dirname(os.path.abspath(__file__)) +search = os.path.join(path, "*.py") + +for f in glob.iglob(search): + f = os.path.basename(f) + n, _ = os.path.splitext(f) + if f == "__init__.py": + continue + importlib.import_module(n, package="src.roles") + +# vim: set sw=4 expandtab: diff --git a/src/roles/seer.py b/src/roles/seer.py new file mode 100644 index 0000000..b60fe24 --- /dev/null +++ b/src/roles/seer.py @@ -0,0 +1,130 @@ +import re + +import src.settings as var +from src.utilities import * +from src import debuglog, errlog, plog +from src.decorators import cmd, event_listener +from src.messages import messages +from src.events import Event + +SEEN = set() + +@cmd("see", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("seer", "oracle", "augur")) +def see(cli, nick, chan, rest): + """Use your paranormal powers to determine the role or alignment of a player.""" + role = get_role(nick) + if nick in SEEN: + pm(cli, nick, messages["seer_fail"]) + return + victim = get_victim(cli, nick, re.split(" +",rest)[0], False) + if not victim: + return + + if victim == nick: + pm(cli, nick, messages["no_see_self"]) + return + + evt = Event("targeted_command", {"target": victim, "misdirection": True, "exchange": True}) + evt.dispatch(cli, var, "see", nick, victim, frozenset("info", "immediate")) + if evt.prevent_default: + return + victim = evt.data["victim"] + victimrole = get_role(victim) + + evt = Event("see", {"role": victimrole}) + evt.dispatch(cli, var, nick, victim) + victimrole = evt.data["role"] + vrole = victimrole # keep a copy for logging + + if role == "seer": + if (victimrole in var.SEEN_WOLF and victimrole not in var.SEEN_DEFAULT) or victim in var.ROLES["cursed villager"]: + victimrole = "wolf" + elif victimrole in var.SEEN_DEFAULT: + victimrole = var.DEFAULT_ROLE + if var.DEFAULT_SEEN_AS_VILL: + victimrole = "villager" + # TODO: split off into shaman.py + if (victim in var.DECEIVED) ^ (nick in var.DECEIVED): + if victimrole == "wolf": + victimrole = "villager" + else: + victimrole = "wolf" + + pm(cli, nick, (messages["seer_success"]).format(victim, victimrole)) + debuglog("{0} ({1}) SEE: {2} ({3}) as {4}".format(nick, role, victim, vrole, victimrole)) + elif role == "oracle": + iswolf = False + if (victimrole in var.SEEN_WOLF and victimrole not in var.SEEN_DEFAULT) or victim in var.ROLES["cursed villager"]: + iswolf = True + # deceit totem acts on both target and actor, so if both have them, they cancel each other out + if (victim in var.DECEIVED) ^ (nick in var.DECEIVED): + iswolf = not iswolf + pm(cli, nick, (messages["oracle_success"]).format(victim, "" if iswolf else "\u0002not\u0002 ", "\u0002" if iswolf else "")) + debuglog("{0} ({1}) SEE: {2} ({3}) (Wolf: {4})".format(nick, role, victim, vrole, str(iswolf))) + elif role == "augur": + if victimrole == "amnesiac": + victimrole = var.AMNESIAC_ROLES[victim] + aura = "blue" + if victimrole in var.WOLFTEAM_ROLES: + aura = "red" + elif victimrole in var.TRUE_NEUTRAL_ROLES: + aura = "grey" + pm(cli, nick, (messages["augur_success"]).format(victim, aura)) + debuglog("{0} ({1}) SEE: {2} ({3}) as {4} ({5} aura)".format(nick, role, victim, vrole, victimrole, aura)) + + SEEN.add(nick) + chk_nightdone(cli) + +@event_listener("rename_player") +def on_rename(evt, cli, var, prefix, nick): + if prefix in SEEN: + SEEN.remove(prefix) + SEEN.add(nick) + +@event_listener("exchange_roles") +def on_exchange(evt, cli, var, actor, nick, actor_role, nick_role): + if actor_role in ("seer", "oracle", "augur"): + SEEN.discard(actor) + +@event_listener("chk_nightdone") +def on_chk_nightdone(evt, cli, var): + evt.data["actedcount"] += len(SEEN) + evt.data["nightroles"].extend(get_roles("seer", "oracle", "augur")) + +@event_listener("transition_night_end", priority=2) +def on_transition_night_end(evt, cli, var): + for seer in list_players(("seer", "oracle", "augur")): + pl = list_players() + random.shuffle(pl) + role = get_role(seer) + pl.remove(seer) # remove self from list + + a = "a" + if role in ("oracle", "augur"): + a = "an" + + if role == "seer": + what = messages["seer_ability"] + elif role == "oracle": + what = messages["oracle_ability"] + elif role == "augur": + what = messages["augur_ability"] + else: + what = messages["seer_role_bug"] + + if seer in var.PLAYERS and not is_user_simple(seer): + pm(cli, seer, messages["seer_role_info"].format(a, role, what)) + else: + pm(cli, seer, messages["seer_simple"].format(a, role)) # !simple + pm(cli, seer, "Players: " + ", ".join(pl)) + + +@event_listener("begin_day") +def on_begin_day(evt, cli, var): + SEEN = set() + +@event_listener("reset") +def on_reset(evt, var): + SEEN = set() + +# vim: set sw=4 expandtab: diff --git a/src/roles/wildchild.py b/src/roles/wildchild.py new file mode 100644 index 0000000..92f2a07 --- /dev/null +++ b/src/roles/wildchild.py @@ -0,0 +1,143 @@ +import src.settings as var +from src.utilities import * +from src import debuglog, errlog, plog +from src.decorators import cmd, event_listener +from src.messages import messages +from src.events import Event + +WILD_CHILDREN = set() +IDOLS = {} + +@cmd("choose", chan=False, pm=True, playing=True, phases=("night",), roles=("wild child",)) +def choose_idol(cli, nick, chan, rest): + """Pick your idol, if they die, you'll become a wolf!""" + if not var.FIRST_NIGHT: + return + if nick in IDOLS: + pm(cli, nick, messages["wild_child_already_picked"]) + return + + victim = get_victim(cli, nick, re.split(" +", rest)[0], False) + if not victim: + return + + if nick == victim: + pm(cli, nick, messages["no_target_self"]) + return + + IDOLS[nick] = victim + pm(cli, nick, messages["wild_child_success"].format(victim)) + + debuglog("{0} ({1}) IDOLIZE: {2} ({3})".format(nick, get_role(nick), victim, get_role(victim))) + chk_nightdone(cli) + +@event_listener("see") +def on_see(evt, cli, var, seer, victim): + if get_role(seer) != "augur" and victim in WILD_CHILDREN: + evt.data["role"] = "wild child" + +@event_listener("rename_player") +def on_rename(evt, cli, var, prefix, nick): + if prefix in WILD_CHILDREN: + WILD_CHILDREN.remove(prefix) + WILD_CHILDREN.add(nick) + +@event_listener("exchange_roles") +def on_exchange(evt, cli, var, actor, nick, actor_role, nick_role): + if actor_role == "wolf" and actor in WILD_CHILDREN and nick not in WILD_CHILDREN: + WILD_CHILDREN.discard(actor) + WILD_CHILDREN.add(nick) + elif actor_role == "wild child": + if nick_role == "wild child": + temp = IDOLS[nick] + IDOLS[nick] = IDOLS[actor] + IDOLS[actor] = temp + evt.data["actor_messages"].append(messages["wild_child_idol"].format(IDOLS[actor])) + evt.data["nick_messages"].append(messages["wild_child_idol"].format(IDOLS[nick])) + else: + IDOLS[nick] = IDOLS.pop(actor) + evt.data["nick_messages"].append(messages["wild_child_idol"].format(IDOLS[nick])) + if nick_role == "wolf" and nick in WILD_CHILDREN and actor not in WILD_CHILDREN: + WILD_CHILDREN.discard(nick) + WILD_CHILDREN.add(actor) + elif nick_role == "wild child" and actor_role != "wild child": + # if they're both wild children, already swapped idols above + IDOLS[actor] = IDOLS.pop(nick) + evt.data["actor_messages"].append(messages["wild_child_idol"].format(IDOLS[actor])) + +@event_listener("myrole") +def on_myrole(evt, cli, var, nick, role): + if role == "wild child" and nick in IDOLS: + pm(cli, nick, messages["wild_child_idol"].format(IDOLS[nick])) + +@event_listener("del_player") +def on_del_player(evt, cli, var, nick, nickrole, nicktpls, lynched, end_game, death_triggers, killer_role, deadlist, original, ismain, refresh_pl): + if var.PHASE not in var.GAME_PHASES: + return + + for child in var.ROLES["wild child"].copy(): + if child not in IDOLS or child in deadlist or IDOLS[child] not in deadlist: + continue + + pm(cli, child, messages["idol_died"]) + WILD_CHILDREN.add(child) + var.ROLES["wild child"].remove(child) + var.ROLES["wolf"].add(child) + var.FINAL_ROLES[child] = "wolf" + wolves = list_players(var.WOLFCHAT_ROLES) + wolves.remove(child) + mass_privmsg(cli, wolves, messages["wild_child_as_wolf"].format(child)) + if var.PHASE == "day": + random.shuffle(wolves) + for i, wolf in enumerate(wolves): + wolfroles = get_role(wolf) + cursed = "" + if wolf in var.ROLES["cursed villager"]: + cursed = "cursed " + wolves[i] = "\u0002{0}\u0002 ({1}{2})".format(wolf, cursed, wolfroles) + + if wolves: + pm(cli, child, "Wolves: " + ", ".join(wolves)) + else: + pm(cli, child, messages["no_other_wolves"]) + +@event_listener("chk_nightdone") +def on_chk_nightdone(evt, cli, var): + if var.FIRST_NIGHT: + evt.data["actedcount"] += len(IDOLS.keys()) + evt.data["nightroles"].extend(var.ROLES["wild child"]) + +@event_listener("transition_day_begin") +def on_transition_day_begin(evt, cli, var): + if (not var.START_WITH_DAY or not var.FIRST_DAY) and var.FIRST_NIGHT: + for child in var.ROLES["wild child"]: + if child not in IDOLS: + pl = list_players() + pl.remove(child) + if pl: + target = random.choice(pl) + IDOLS[child] = target + pm(cli, child, messages["wild_child_random_idol"].format(target)) + +@event_listener("transition_night_end", priority=2) +def on_transition_night_end(evt, cli, var): + for child in var.ROLES["wild child"]: + if child in var.PLAYERS and not is_user_simple(child): + pm(cli, child, messages["child_notify"]) + else: + pm(cli, child, messages["child_simple"]) + +@event_listener("revealroles_role") +def on_revealroles_role(evt, cli, var, nick, role): + if role == "wild child": + if nick in IDOLS: + evt.data["special_case"].append("picked {0} as idol".format(IDOLS[nick])) + else: + evt.data["special_case"].append("no idol picked yet") + +@event_listener("reset") +def on_reset(evt, var): + WILD_CHILDREN = set() + IDOLS = {} + +# vim: set sw=4 expandtab: diff --git a/src/wolfgame.py b/src/wolfgame.py index c385158..cf71536 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -54,6 +54,7 @@ Event = events.Event cmd = decorators.cmd hook = decorators.hook handle_error = decorators.handle_error +event_listener = decorators.event_listener COMMANDS = decorators.COMMANDS # Game Logic Begins: @@ -410,7 +411,6 @@ def reset(): var.START_VOTES = set() # list of players who have voted to !start var.LOVERS = {} # need to be here for purposes of random var.ENTRANCED = set() - var.WILD_CHILDREN = set() reset_settings() @@ -421,6 +421,9 @@ def reset(): var.SPECTATING_WOLFCHAT = set() var.SPECTATING_DEADCHAT = set() + evt = Event("reset", {}) + evt.dispatch(var) + reset() @cmd("fsync", flag="m", pm=True) @@ -2850,7 +2853,7 @@ def chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death_triggers=True, killer_role="", deadlist=[], original="", cmode=[], deadchat=[], ismain=True): """ Returns: False if one side won. - arg: forced_death = True when lynched or when the seer/wolf both don't act + arg: forced_death = True when lynched """ def refresh_pl(old_pl): @@ -2943,31 +2946,6 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death if nickrole == "clone" and nick in var.CLONED: del var.CLONED[nick] - for child in var.ROLES["wild child"].copy(): - if child in var.IDOLS and child not in deadlist: - if var.IDOLS[child] in deadlist: - pm(cli, child, messages["idol_died"]) - var.WILD_CHILDREN.add(child) - var.ROLES["wild child"].remove(child) - var.ROLES["wolf"].add(child) - var.FINAL_ROLES[child] = "wolf" - wolves = list_players(var.WOLFCHAT_ROLES) - wolves.remove(child) - mass_privmsg(cli, wolves, messages["wild_child_as_wolf"].format(child)) - if var.PHASE == "day": - random.shuffle(wolves) - for i, wolf in enumerate(wolves): - wolfroles = get_role(wolf) - cursed = "" - if wolf in var.ROLES["cursed villager"]: - cursed = "cursed " - wolves[i] = "\u0002{0}\u0002 ({1}{2})".format(wolf, cursed, wolfroles) - - if wolves: - pm(cli, child, "Wolves: " + ", ".join(wolves)) - else: - pm(cli, child, messages["no_other_wolves"]) - if death_triggers and var.PHASE in var.GAME_PHASES: if nick in var.LOVERS: others = var.LOVERS[nick].copy() @@ -3629,7 +3607,7 @@ def rename_player(cli, prefix, nick): if prefix in dictvar.keys(): del dictvar[prefix] for dictvar in (var.VENGEFUL_GHOSTS, var.TOTEMS, var.FINAL_ROLES, var.GUNNERS, var.TURNCOATS, - var.DOCTORS, var.BITTEN_ROLES, var.LYCAN_ROLES, var.AMNESIAC_ROLES, var.IDOLS): + var.DOCTORS, var.BITTEN_ROLES, var.LYCAN_ROLES, var.AMNESIAC_ROLES): if prefix in dictvar.keys(): dictvar[nick] = dictvar.pop(prefix) # Looks like {'jacob2': ['5'], '7': ['3']} @@ -3675,7 +3653,7 @@ def rename_player(cli, prefix, nick): var.DISEASED, var.TOBEDISEASED, var.RETRIBUTION, var.MISDIRECTED, var.TOBEMISDIRECTED, var.EXCHANGED, var.IMMUNIZED, var.CURED_LYCANS, var.ALPHA_WOLVES, var.CURSED, var.CHARMERS, var.CHARMED, var.TOBECHARMED, var.PRIESTS, var.CONSECRATING, var.ENTRANCED_DYING, var.DYING, - var.DECEIVED, var.WILD_CHILDREN): + var.DECEIVED): if prefix in setvar: setvar.remove(prefix) setvar.add(nick) @@ -3904,7 +3882,7 @@ def begin_day(cli): var.KILLS = {} # nicknames of kill victims (wolves only) var.OTHER_KILLS = {} # other kill victims (hunter/vengeful ghost) var.KILLER = "" # nickname of who chose the victim - var.SEEN = set() # set of seers/oracles/augurs that have had visions + var.SEEN = set() # set of doomsayers that have had visions var.HEXED = set() # set of hags that have silenced others var.SHAMANS = {} # dict of shamans/crazed shamans that have acted and who got totems var.OBSERVED = {} # those whom werecrows/sorcerers have observed @@ -4054,16 +4032,6 @@ def transition_day(cli, gameid=0): choose.func(cli, mm, mm, lovers[0] + " " + lovers[1], sendmsg=False) pm(cli, mm, messages["random_matchmaker"]) - for child in var.ROLES["wild child"]: - if child not in var.IDOLS: - ps = pl[:] - pl.remove(child) - if ps: - target = random.choice(ps) - var.IDOLS[child] = target - pm(cli, child, messages["wild_child_random_idol"].format(target)) - - # Reset daytime variables var.INVESTIGATED = set() var.WOUNDED = set() @@ -4788,10 +4756,10 @@ def chk_nightdone(cli): var.OTHER_KILLS, var.PASSED, var.OBSERVED, var.HEXED, var.SHAMANS, var.CURSED, var.CHARMERS))) - nightroles = get_roles("seer", "oracle", "harlot", "succubus", "bodyguard", + nightroles = get_roles("harlot", "succubus", "bodyguard", "guardian angel", "wolf", "werecrow", "alpha wolf", "sorcerer", "hunter", "hag", "shaman", "crazed shaman", - "augur", "werekitten", "warlock", "piper", "wolf mystic", + "werekitten", "warlock", "piper", "wolf mystic", "fallen angel", "vigilante", "doomsayer", "doomsayer", # NOT a mistake, doomsayer MUST be listed twice "prophet", "wolf shaman", "wolf shaman") # wolf shaman also must be listed twice @@ -4814,8 +4782,8 @@ def chk_nightdone(cli): nightroles.append(p) if var.FIRST_NIGHT: - actedcount += len(var.MATCHMAKERS | var.CLONED.keys() | var.IDOLS.keys()) - nightroles.extend(get_roles("matchmaker", "clone", "wild child")) + actedcount += len(var.MATCHMAKERS | var.CLONED.keys()) + nightroles.extend(get_roles("matchmaker", "clone")) if var.DISEASED_WOLVES: nightroles = [p for p in nightroles if p not in list_players(var.WOLF_ROLES - {"wolf cub", "werecrow", "doomsayer"})] @@ -5063,10 +5031,9 @@ def check_exchange(cli, actor, nick): del var.SHAMANS[actor] if actor in var.LASTGIVEN: del var.LASTGIVEN[actor] - elif actor_role in var.WOLF_ROLES - {"werecrow", "wolf cub", "alpha wolf"}: + elif actor_role in var.WOLF_ROLES - {"werecrow", "wolf cub", "alpha wolf", "doomsayer"}: if actor in var.KILLS: del var.KILLS[actor] - var.WILD_CHILDREN.discard(actor) elif actor_role == "hunter": if actor in var.OTHER_KILLS: del var.OTHER_KILLS[actor] @@ -5086,10 +5053,6 @@ def check_exchange(cli, actor, nick): if var.HVISITED[actor] is not None: pm(cli, var.HVISITED[actor], messages["harlot_disappeared"].format(actor)) del var.HVISITED[actor] - elif actor_role in ("seer", "oracle", "augur"): - var.SEEN.discard(actor) - elif actor_role == "wild child": # would that ever happen? - var.IDOLS[nick] = var.IDOLS.pop(actor) elif actor_role == "hag": if actor in var.LASTHEXED: if var.LASTHEXED[actor] in var.TOBESILENCED and actor in var.HEXED: @@ -5105,6 +5068,10 @@ def check_exchange(cli, actor, nick): var.ALPHA_WOLVES.discard(actor) if actor in var.KILLS: del var.KILLS[actor] + elif actor_role == "doomsayer": + var.SEEN.discard(actor) + if actor in var.KILLS: + del var.KILLS[actor] elif actor_role == "warlock": var.CURSED.discard(actor) elif actor_role == "turncoat": @@ -5130,10 +5097,9 @@ def check_exchange(cli, actor, nick): del var.SHAMANS[nick] if nick in var.LASTGIVEN: del var.LASTGIVEN[nick] - elif nick_role in var.WOLF_ROLES - {"werecrow", "wolf cub", "alpha wolf"}: + elif nick_role in var.WOLF_ROLES - {"werecrow", "wolf cub", "alpha wolf", "doomsayer"}: if nick in var.KILLS: del var.KILLS[nick] - var.WILD_CHILDREN.discard(nick) elif nick_role == "hunter": if nick in var.OTHER_KILLS: del var.OTHER_KILLS[nick] @@ -5153,10 +5119,6 @@ def check_exchange(cli, actor, nick): if var.HVISITED[nick] is not None: pm(cli, var.HVISITED[nick], messages["harlot_disappeared"].format(nick)) del var.HVISITED[nick] - elif nick_role in ("seer", "oracle", "augur"): - var.SEEN.discard(nick) - elif nick_role == "wild child": - var.IDOLS[actor] = var.IDOLS.pop(nick) elif nick_role == "hag": if nick in var.LASTHEXED: if var.LASTHEXED[nick] in var.TOBESILENCED and nick in var.HEXED: @@ -5171,11 +5133,17 @@ def check_exchange(cli, actor, nick): var.ALPHA_WOLVES.discard(nick) if nick in var.KILLS: del var.KILLS[nick] + elif nick_role == "doomsayer": + var.SEEN.discard(nick) + if nick in var.KILLS: + del var.KILLS[nick] elif nick_role == "warlock": var.CURSED.discard(nick) elif nick_role == "turncoat": del var.TURNCOATS[nick] + evt = Event("exchange_roles", {"actor_messages": [], "nick_messages": []}) + evt.dispatch(cli, var, actor, nick, actor_role, nick_role) var.FINAL_ROLES[actor] = nick_role var.FINAL_ROLES[nick] = actor_role @@ -5220,6 +5188,11 @@ def check_exchange(cli, actor, nick): pm(cli, actor, messages["role_swap"].format(nick_rev_role)) pm(cli, nick, messages["role_swap"].format(actor_rev_role)) + for msg in evt.data["actor_messages"]: + pm(cli, actor, msg) + for msg in evt.data["nick_messages"]: + pm(cli, nick, msg) + wolfchatwolves = set(var.WOLFCHAT_ROLES) if var.RESTRICT_WOLFCHAT & (var.RW_WOLVES_ONLY_CHAT | var.RW_REM_NON_WOLVES): wolfchatwolves = set(var.WOLF_ROLES) @@ -5230,8 +5203,6 @@ def check_exchange(cli, actor, nick): if nick_role == "clone": pm(cli, actor, messages["clone_target"].format(nick_target)) - elif nick_role == "wild child": - pm(cli, actor, messages["wild_child_idol"].format(var.IDOLS[actor])) elif nick_role in var.TOTEM_ORDER: if nick_role == "shaman": pm(cli, actor, messages["shaman_totem"].format(nick_totem)) @@ -5274,8 +5245,6 @@ def check_exchange(cli, actor, nick): if actor_role == "clone": pm(cli, nick, messages["clone_target"].format(actor_target)) - elif actor_role == "wild child": - pm(cli, nick, messages["wild_child_idol"].format(var.IDOLS[nick])) elif actor_role in var.TOTEM_ORDER: if actor_role == "shaman": pm(cli, nick, messages["shaman_totem"].format(actor_totem)) @@ -5961,9 +5930,9 @@ def hvisit(cli, nick, chan, rest): debuglog("{0} ({1}) VISIT: {2} ({3})".format(nick, role, victim, get_role(victim))) chk_nightdone(cli) -@cmd("see", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("seer", "oracle", "augur", "doomsayer")) +@cmd("see", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=("doomsayer",)) def see(cli, nick, chan, rest): - """Use your paranormal powers to determine the role or alignment of a player.""" + """Use your paranormal senses to determine a player's doom.""" role = get_role(nick) if nick in var.SEEN: pm(cli, nick, messages["seer_fail"]) @@ -5978,7 +5947,7 @@ def see(cli, nick, chan, rest): if is_safe(nick, victim): pm(cli, nick, messages["no_acting_on_succubus"].format("see")) return - if role == "doomsayer" and in_wolflist(nick, victim): + if in_wolflist(nick, victim): pm(cli, nick, messages["no_see_wolf"]) return victim = choose_target(nick, victim) @@ -5986,63 +5955,23 @@ def see(cli, nick, chan, rest): return victimrole = get_role(victim) vrole = victimrole # keep a copy for logging - if role == "seer": - if victim in var.WILD_CHILDREN: - victimrole = "wild child" - elif (victimrole in var.SEEN_WOLF and victimrole not in var.SEEN_DEFAULT) or victim in var.ROLES["cursed villager"]: - victimrole = "wolf" - elif victimrole in var.SEEN_DEFAULT: - victimrole = var.DEFAULT_ROLE - if var.DEFAULT_SEEN_AS_VILL: - victimrole = "villager" - if (victim in var.DECEIVED) ^ (nick in var.DECEIVED): - if victimrole == "wolf": - victimrole = "villager" - else: - victimrole = "wolf" - - pm(cli, nick, (messages["seer_success"]).format(victim, victimrole)) - debuglog("{0} ({1}) SEE: {2} ({3}) as {4}".format(nick, role, victim, vrole, victimrole)) - elif role == "oracle": - iswolf = False - if victim in var.WILD_CHILDREN: - pass - elif (victimrole in var.SEEN_WOLF and victimrole not in var.SEEN_DEFAULT) or victim in var.ROLES["cursed villager"]: - iswolf = True - # deceit totem acts on both target and actor, so if both have them, they cancel each other out - if (victim in var.DECEIVED) ^ (nick in var.DECEIVED): - iswolf = not iswolf - pm(cli, nick, (messages["oracle_success"]).format(victim, "" if iswolf else "\u0002not\u0002 ", "\u0002" if iswolf else "")) - debuglog("{0} ({1}) SEE: {2} ({3}) (Wolf: {4})".format(nick, role, victim, vrole, str(iswolf))) - elif role == "augur": - if victimrole == "amnesiac": - victimrole = var.AMNESIAC_ROLES[victim] - aura = "blue" - if victimrole in var.WOLFTEAM_ROLES: - aura = "red" - elif victimrole in var.TRUE_NEUTRAL_ROLES: - aura = "grey" - pm(cli, nick, (messages["augur_success"]).format(victim, aura)) - debuglog("{0} ({1}) SEE: {2} ({3}) as {4} ({5} aura)".format(nick, role, victim, vrole, victimrole, aura)) - elif role == "doomsayer": - mode = random.randint(1, 3) - if mode == 1: - mode = "DEATH" - pm(cli, nick, messages["doomsayer_death"].format(victim)) - var.OTHER_KILLS[nick] = victim - elif mode == 2: - mode = "LYCAN" - pm(cli, nick, messages["doomsayer_lycan"].format(victim)) - var.TOBELYCANTHROPES.add(victim) - elif mode == 3: - mode = "SICK" - pm(cli, nick, messages["doomsayer_sick"].format(victim)) - var.TOBEDISEASED.add(victim) - var.TOBESILENCED.add(victim) - var.SICK.add(victim) # counts as unable to vote and lets them know at beginning of day that they aren't feeling well - - debuglog("{0} ({1}) SEE: {2} ({3}) - {4}".format(nick, role, victim, vrole, mode)) - relay_wolfchat_command(cli, nick, messages["doomsayer_wolfchat"].format(nick, victim), ("doomsayer",), is_wolf_command=True) + mode = random.randint(1, 3) + if mode == 1: + mode = "DEATH" + pm(cli, nick, messages["doomsayer_death"].format(victim)) + var.OTHER_KILLS[nick] = victim + elif mode == 2: + mode = "LYCAN" + pm(cli, nick, messages["doomsayer_lycan"].format(victim)) + var.TOBELYCANTHROPES.add(victim) + elif mode == 3: + mode = "SICK" + pm(cli, nick, messages["doomsayer_sick"].format(victim)) + var.TOBEDISEASED.add(victim) + var.TOBESILENCED.add(victim) + var.SICK.add(victim) # counts as unable to vote and lets them know at beginning of day that they aren't feeling well + debuglog("{0} ({1}) SEE: {2} ({3}) - {4}".format(nick, role, victim, vrole, mode)) + relay_wolfchat_command(cli, nick, messages["doomsayer_wolfchat"].format(nick, victim), ("doomsayer",), is_wolf_command=True) var.SEEN.add(nick) chk_nightdone(cli) @@ -6512,28 +6441,23 @@ def charm(cli, nick, chan, rest): chk_nightdone(cli) -@cmd("choose", chan=False, pm=True, playing=True, phases=("night",), roles=("wild child",)) -def choose_idol(cli, nick, chan, rest): - """Pick your idol, if they die, you'll become a wolf!""" - if not var.FIRST_NIGHT: - return - if nick in var.IDOLS: - pm(cli, nick, messages["wild_child_already_picked"]) +@event_listener("targeted_cmd", priority=9) +def on_targeted_cmd(evt, cli, var, cmd, actor, orig_target, tags): + # TODO: move beneficial command check into roles/succubus.py + if "beneficial" not in tags and is_safe(actor, evt.data["target"]): + pm(cli, actor, messages["no_acting_on_succubus"].format(cmd)) + evt.stop_processing = True + evt.prevent_default = True return - victim = get_victim(cli, nick, re.split(" +", rest)[0], False) - if not victim: + if evt.data["misdirection"]: + evt.data["target"] = choose_target(actor, evt.data["target"]) + + if evt.data["exchange"] and check_exchange(cli, actor, evt.data["target"]): + evt.stop_processing = True + evt.prevent_default = True return - if nick == victim: - pm(cli, nick, messages["no_target_self"]) - return - - var.IDOLS[nick] = victim - pm(cli, nick, messages["wild_child_success"].format(victim)) - - debuglog("{0} ({1}) IDOLIZE: {2} ({3})".format(nick, get_role(nick), victim, get_role(victim))) - chk_nightdone(cli) @hook("featurelist") # For multiple targets with PRIVMSG def getfeatures(cli, nick, *rest): @@ -6865,31 +6789,6 @@ def transition_night(cli): if var.ALPHA_ENABLED and role == "alpha wolf" and wolf not in var.ALPHA_WOLVES: pm(cli, wolf, messages["wolf_bite"]) - for seer in list_players(("seer", "oracle", "augur")): - pl = ps[:] - random.shuffle(pl) - role = get_role(seer) - pl.remove(seer) # remove self from list - - a = "a" - if role in ("oracle", "augur"): - a = "an" - - if role == "seer": - what = messages["seer_ability"] - elif role == "oracle": - what = messages["oracle_ability"] - elif role == "augur": - what = messages["augur_ability"] - else: - what = messages["seer_role_bug"] - - if seer in var.PLAYERS and not is_user_simple(seer): - pm(cli, seer, messages["seer_role_info"].format(a, role, what)) - else: - pm(cli, seer, messages["seer_simple"].format(a, role)) # !simple - pm(cli, seer, "Players: " + ", ".join(pl)) - for harlot in var.ROLES["harlot"]: pl = ps[:] random.shuffle(pl) @@ -7167,12 +7066,6 @@ def transition_night(cli): else: pm(cli, lycan, messages["lycan_simple"]) - for child in var.ROLES["wild child"]: - if child in var.PLAYERS and not is_user_simple(child): - pm(cli, child, messages["child_notify"]) - else: - pm(cli, child, messages["child_simple"]) - for v_ghost, who in var.VENGEFUL_GHOSTS.items(): if who[0] == "!": continue @@ -7588,7 +7481,6 @@ def start(cli, nick, chan, forced = False, restart = ""): var.SICK = set() var.DULLAHAN_TARGETS = {} var.DECEIVED = set() - var.IDOLS = {} var.DEADCHAT_PLAYERS = set() var.SPECTATING_WOLFCHAT = set() @@ -8364,9 +8256,17 @@ def myrole(cli, nick, chan, rest): role = "villager" elif role in ("amnesiac", "vengeful ghost"): role = var.DEFAULT_ROLE + + evt = Event("myrole", {"role": role, "messages": []}) + evt.dispatch(cli, var, nick) + role = evt.data["role"] + an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" pm(cli, nick, messages["show_role"].format(an, role)) + for msg in evt.data["messages"]: + pm(cli, nick, msg) + # Remind shamans what totem they have if role in var.TOTEM_ORDER and role != "crazed shaman" and var.PHASE == "night" and nick not in var.SHAMANS: pm(cli, nick, messages["totem_simple"].format(var.TOTEMS[nick])) @@ -8375,9 +8275,6 @@ def myrole(cli, nick, chan, rest): if role == "clone" and nick in var.CLONED: pm(cli, nick, messages["clone_target"].format(var.CLONED[nick])) - if role == "wild child" and nick in var.IDOLS: - pm(cli, nick, messages["wild_child_idol"].format(var.IDOLS[nick])) - # Give minion the wolf list they would have recieved night one if role == "minion": wolves = [] @@ -8861,12 +8758,12 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: special_case.append("need to kill {0}".format(", ".join(var.DULLAHAN_TARGETS[nickname] - var.DEAD))) else: special_case.append("All targets dead") - elif role == "wild child": - if nickname in var.IDOLS: - special_case.append("picked {0} as idol".format(var.IDOLS[nickname])) - else: - special_case.append("no idol picked yet") - if nickname not in var.ORIGINAL_ROLES[role] and role not in var.TEMPLATE_RESTRICTIONS: + + evt = Event("revealroles_role", {"special_case": special_case}) + evt.dispatch(cli, var, nickname, role) + special_case = evt.data["special_case"] + + if not evt.prevent_default and nickname not in var.ORIGINAL_ROLES[role] and role not in var.TEMPLATE_RESTRICTIONS: for old_role in role_order(): # order doesn't matter here, but oh well if nickname in var.ORIGINAL_ROLES[old_role] and nickname not in var.ROLES[old_role]: special_case.append("was {0}".format(old_role)) @@ -8913,6 +8810,9 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: if var.CHARMED | var.TOBECHARMED: output.append("\u0002charmed players\u0002: {0}".format(", ".join(var.CHARMED | var.TOBECHARMED))) + evt = Event("revealroles", {"output": output}) + evt.dispatch(cli, var) + if botconfig.DEBUG_MODE: cli.msg(chan, break_long_message(output, " | ")) else: