diff --git a/messages/en.json b/messages/en.json index 7b79da9..d1ed00d 100644 --- a/messages/en.json +++ b/messages/en.json @@ -432,7 +432,7 @@ "day_lasted": "Day lasted \u0002{0:0>2}:{1:0>2}\u0002. ", "fallen_angel_turn": "As the moonlight filters through your window, you think back on the past few days. Your power has been growing, but the villagers you protect subconsciously detected your shift and have been keeping more distant from you. Grinning with wicked resolve, you vow to show them what fools they have been as you take to the skies once more with an unholy vengeance. Soon they will know true fear.", "bitten_turn": "As you prepare for bed, you watch in horror as your body starts growing a coat of fur! Sudden realization hits you as you grin with your now muzzled face; that mysterious bite earlier slowly changed you into a werewolf! You feel bigger, stronger, faster, and ready to seize the night as you stealthily exit your home and search for the rest of your pack...", - "bitten_turn_wolfchat": "\u0002{0}\u0002 is now a \u0002{1}\u0002!", + "wolfchat_new_member": "\u0002{0}\u0002 is now a \u0002{1}\u0002!", "amnesia_clear": "Your amnesia clears and you now remember that you are a{0} \u0002{1}\u0002!", "amnesia_wolfchat": "\u0002{0}\u0002 is now a \u0002{1}\u0002!", "wolf_notify": "You are a \u0002wolf\u0002. It is your job to kill all the villagers. Use \"kill \" to kill a villager.", diff --git a/src/events.py b/src/events.py index e90aebe..c6b059a 100644 --- a/src/events.py +++ b/src/events.py @@ -24,12 +24,12 @@ class Event: self.name = name self.data = data - def dispatch(self, *args): + def dispatch(self, *args, **kwargs): if self.name not in EVENT_CALLBACKS: return True for item in list(EVENT_CALLBACKS[self.name]): - item[1](self, *args) + item[1](self, *args, **kwargs) if self.stop_processing: break diff --git a/src/gamemodes.py b/src/gamemodes.py index 0a78efd..fb29d6d 100644 --- a/src/gamemodes.py +++ b/src/gamemodes.py @@ -1055,14 +1055,63 @@ class MaelstromMode(GameMode): self.LOVER_WINS_WITH_FOOL = True self.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS = 0 # always make it happen self.ALWAYS_PM_ROLE = True + # clone is pointless in this mode + # dullahan doesn't really work in this mode either, if enabling anyway special logic to determine kill list + # needs to be added above for when dulls are added during the game + # matchmaker is conditionally enabled during night 1 only + self.roles = list(var.ROLE_GUIDE.keys() - var.TEMPLATE_RESTRICTIONS.keys() - {"amnesiac", "clone", "dullahan", "matchmaker"}) def startup(self): events.add_listener("role_attribution", self.role_attribution) events.add_listener("transition_night_begin", self.transition_night_begin) + events.add_listener("join", self.on_join) def teardown(self): events.remove_listener("role_attribution", self.role_attribution) events.remove_listener("transition_night_begin", self.transition_night_begin) + events.remove_listener("join", self.on_join) + + def on_join(self, evt, cli, var, nick, chan, rest, forced=False): + if var.PHASE != "day" or (nick != chan and chan != botconfig.CHANNEL): + return + if not forced and evt.data["join_player"](cli, nick, botconfig.CHANNEL, sanity=False): + self._on_join(cli, var, nick, chan) + evt.prevent_default = True + elif forced: + # in fjoin, handle this differently + jp = evt.data["join_player"] + evt.data["join_player"] = lambda cli, nick, chan, who=None, forced=False: jp(cli, nick, chan, who=who, forced=forced, sanity=False) and self._on_join(cli, var, nick, chan) + + def _on_join(self, cli, var, nick, chan): + role = random.choice(self.roles) + var.ROLES[role].add(nick) + var.ORIGINAL_ROLES[role].add(nick) + var.FINAL_ROLES[nick] = role + if role == "doctor": + lpl = len(var.list_players()) + var.DOCTORS[nick] = math.ceil(var.DOCTOR_IMMUNIZATION_MULTIPLIER * lpl) + # let them know their role + # FIXME: this is fugly + from src.decorators import COMMANDS + COMMANDS["myrole"][0].caller(cli, nick, chan, "") + # if they're a wolfchat role, alert the other wolves + if role in var.WOLFCHAT_ROLES: + relay_wolfchat_command(cli, nick, messages["wolfchat_new_member"].format(nick, role), var.WOLFCHAT_ROLES, is_wolf_command=True, is_kill_command=True) + # TODO: make this part of !myrole instead, no reason we can't give out wofllist in that + wolves = var.list_players(var.WOLFCHAT_ROLES) + pl = var.list_players() + random.shuffle(pl) + pl.remove(nick) + for i, player in enumerate(pl): + prole = var.get_role(player) + if prole in var.WOLFCHAT_ROLES: + cursed = "" + if player in var.ROLES["cursed villager"]: + cursed = "cursed " + pl[i] = "\u0002{0}\u0002 ({1}{2})".format(player, cursed, prole) + elif player in var.ROLES["cursed villager"]: + pl[i] = player + " (cursed)" + pm(cli, nick, "Players: " + ", ".join(pl)) def role_attribution(self, evt, cli, chk_win_conditions, var, villagers): self.chk_win_conditions = chk_win_conditions @@ -1112,13 +1161,10 @@ class MaelstromMode(GameMode): wolves = var.WOLF_ROLES - {"wolf cub"} addroles[random.choice(list(wolves))] += 1 # make sure there's at least one wolf role - # clone is pointless in this mode - # dullahan doesn't really work in this mode either, if enabling anyway special logic to determine kill list - # needs to be added above for when dulls are added during the game - roles = list(var.ROLE_GUIDE.keys() - var.TEMPLATE_RESTRICTIONS.keys() - {"amnesiac", "clone", "dullahan"}) - if not do_templates: + roles = self.roles[:] + if do_templates: # mm only works night 1, do_templates is also only true n1 - roles.remove("matchmaker") + self.roles.append("matchmaker") while lpl: addroles[random.choice(roles)] += 1 lpl -= 1 diff --git a/src/wolfgame.py b/src/wolfgame.py index 114d199..d5da646 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -1118,6 +1118,14 @@ def deadchat_pref(cli, nick, chan, rest): @cmd("join", "j", pm=True) def join(cli, nick, chan, rest): """Either starts a new game of Werewolf or joins an existing game that has not started yet.""" + # keep this and the event in fjoin() in sync + evt = Event("join", { + "join_player": join_player, + "join_deadchat": join_deadchat, + "vote_gamemode": vote_gamemode + }) + if not evt.dispatch(cli, var, nick, chan, rest, forced=False): + return if var.PHASE in ("none", "join"): if chan == nick: return @@ -1125,25 +1133,25 @@ def join(cli, nick, chan, rest): if nick in var.USERS and (not var.USERS[nick]["account"] or var.USERS[nick]["account"] == "*"): cli.notice(nick, messages["not_logged_in"]) return - if join_player(cli, nick, chan) and rest: - vote_gamemode(cli, nick, chan, rest.lower().split()[0], False) + if evt.data["join_player"](cli, nick, chan) and rest: + evt.data["vote_gamemode"](cli, nick, chan, rest.lower().split()[0], False) else: # join deadchat if chan == nick and nick != botconfig.NICK: - join_deadchat(cli, nick) + evt.data["join_deadchat"](cli, nick) -def join_player(cli, player, chan, who = None, forced = False): +def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): if who is None: who = player pl = var.list_players() if chan != botconfig.CHANNEL: - return + return False if not var.OPPED: cli.notice(who, messages["bot_not_opped"].format(chan)) cli.msg("ChanServ", "op " + botconfig.CHANNEL) - return + return False if player in var.USERS: ident = var.USERS[player]["ident"] @@ -1155,7 +1163,7 @@ def join_player(cli, player, chan, who = None, forced = False): host = None acc = None else: - return # Not normal + return False # Not normal if not acc or acc == "*" or var.DISABLE_ACCOUNTS: acc = None @@ -1174,11 +1182,10 @@ def join_player(cli, player, chan, who = None, forced = False): cli.notice(who, messages["stasis"].format( "you are" if player == who else player + " is", stasis, "s" if stasis != 1 else "")) - return + return False cmodes = [("+v", player)] if var.PHASE == "none": - if var.AUTO_TOGGLE_MODES and player in var.USERS and var.USERS[player]["modes"]: for mode in var.USERS[player]["modes"]: cmodes.append(("-"+mode, player)) @@ -1210,13 +1217,16 @@ def join_player(cli, player, chan, who = None, forced = False): elif player in pl: cli.notice(who, messages["already_playing"].format("You" if who == player else "They")) - return True + # if we're not doing insane stuff, return True so that one can use !join to vote for a game mode + # even if they are already joined. If we ARE doing insane stuff, return False to indicate that + # the player was not successfully joined by this call. + return sanity elif len(pl) >= var.MAX_PLAYERS: cli.notice(who, messages["too_many_players"]) - return - elif var.PHASE != "join": + return False + elif sanity and var.PHASE != "join": cli.notice(who, messages["game_already_running"]) - return + return False else: if acc is not None and not botconfig.DEBUG_MODE: for user in pl: @@ -1228,7 +1238,6 @@ def join_player(cli, player, chan, who = None, forced = False): cli.notice(who, msg.format(user, "their", "")) return - var.ROLES["person"].add(player) var.ALL_PLAYERS.append(player) if not is_fake_nick(player) or not botconfig.DEBUG_MODE: if var.AUTO_TOGGLE_MODES and var.USERS[player]["modes"]: @@ -1238,6 +1247,13 @@ def join_player(cli, player, chan, who = None, forced = False): var.USERS[player]["modes"] = set() mass_mode(cli, cmodes, []) cli.msg(chan, messages["player_joined"].format(player, len(pl) + 1)) + if not sanity: + # Abandon Hope All Ye Who Enter Here + leave_deadchat(cli, player) + var.SPECTATING_DEADCHAT.discard(player) + var.SPECTATING_WOLFCHAT.discard(player) + return True + var.ROLES["person"].add(player) if not is_fake_nick(player): hostmask = ident + "@" + host if hostmask not in var.JOINED_THIS_GAME and (not acc or acc not in var.JOINED_THIS_GAME_ACCS): @@ -1287,9 +1303,17 @@ def kill_join(cli, chan): var.AFTER_FLASTGAME = None -@cmd("fjoin", admin_only=True, phases=("none", "join")) +@cmd("fjoin", admin_only=True) def fjoin(cli, nick, chan, rest): """Forces someone to join a game.""" + # keep this and the event in def join() in sync + evt = Event("join", { + "join_player": join_player, + "join_deadchat": join_deadchat, + "vote_gamemode": vote_gamemode + }) + if not evt.dispatch(cli, var, nick, chan, rest, forced=True): + return noticed = False fake = False if not var.OPPED: @@ -1297,7 +1321,7 @@ def fjoin(cli, nick, chan, rest): cli.msg("ChanServ", "op " + botconfig.CHANNEL) return if not rest.strip(): - join_player(cli, nick, chan, forced=True) + evt.data["join_player"](cli, nick, chan, forced=True) for tojoin in re.split(" +",rest): tojoin = tojoin.strip() @@ -1309,7 +1333,7 @@ def fjoin(cli, nick, chan, rest): break fake = True for i in range(int(first), int(last)+1): - join_player(cli, str(i), chan, forced=True, who=nick) + evt.data["join_player"](cli, str(i), chan, forced=True, who=nick) continue if not tojoin: continue @@ -1330,7 +1354,7 @@ def fjoin(cli, nick, chan, rest): elif botconfig.DEBUG_MODE: fake = True if tojoin != botconfig.NICK: - join_player(cli, tojoin, chan, forced=True, who=nick) + evt.data["join_player"](cli, tojoin, chan, forced=True, who=nick) else: cli.notice(nick, messages["not_allowed"]) if fake: @@ -6636,7 +6660,7 @@ def transition_night(cli): var.ROLES[chumprole].remove(chump) var.ROLES[newrole].add(chump) var.FINAL_ROLES[chump] = newrole - relay_wolfchat_command(cli, chump, messages["bitten_turn_wolfchat"].format(chump, newrole), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True) + relay_wolfchat_command(cli, chump, messages["wolfchat_new_member"].format(chump, newrole), var.WOLF_ROLES, is_wolf_command=True, is_kill_command=True) # convert amnesiac if var.NIGHT_COUNT == var.AMNESIAC_NIGHTS: