diff --git a/messages/en.json b/messages/en.json index 1dd80e9..5d348bd 100644 --- a/messages/en.json +++ b/messages/en.json @@ -198,7 +198,7 @@ "lover_suicide_no_reveal": "Saddened by the loss of their lover, \u0002{0}\u0002 commits suicide.", "assassin_fail_totem": "Before dying, \u0002{0}\u0002 quickly attempts to slit \u0002{1}\u0002's throat; however, {1}'s totem emits a brilliant flash of light, causing the attempt to miss.", "assassin_fail_angel": "Before dying, \u0002{0}\u0002 quickly attempts to slit \u0002{1}\u0002's throat; however, a guardian angel was on duty and able to foil the attempt.", - "assassin_fail_bodyguard": "Before dying, \u0002{0}\u0002 quickly attempts to slit \u0002{1}\u0002's throat; however, \u0002{2}\u0002, a bodyguard, sacrificed their life to protect them.", + "assassin_fail_bodyguard": "Sensing danger, \u0002{2}\u0002 shoves \u0002{1}\u0002 aside to save them from \u0002{0}\u0002.", "assassin_success": "Before dying, \u0002{0}\u0002 quickly slits \u0002{1}\u0002's throat. The village mourns the loss of a{2} \u0002{3}\u0002.", "assassin_success_no_reveal": "Before dying, \u0002{0}\u0002 quickly slits \u0002{1}\u0002's throat.", "time_lord_dead": "Tick tock! Since the time lord has died, day will now only last {0} seconds and night will now only last {1} seconds!", @@ -209,6 +209,9 @@ "mad_scientist_kill_single": "\u0002{0}\u0002 throws a potent chemical concoction into the crowd. \u0002{1}\u0002, a{2} \u0002{3}\u0002, gets hit by the chemicals and dies.", "mad_scientist_kill_single_no_reveal": "\u0002{0}\u0002 throws a potent chemical concoction into the crowd. \u0002{1}\u0002 gets hit by the chemicals and dies.", "mad_scientist_fail": "\u0002{0}\u0002 throws a potent chemical concoction into the crowd. Thankfully, nobody seems to have gotten hit.", + "mad_scientist_fail_totem": "Sensing danger, \u0002{1}\u0002's totem emits a brilliant flash of light, teleporting them away from \u0002{0}\u0002.", + "mad_scientist_fail_angel": "Sensing danger, a guardian angel whisks \u0002{1}\u0002 away from \u0002{0}\u0002.", + "mad_scientist_fail_bodyguard": "Sensing danger, \u0002{2}\u0002 shoves \u0002{1}\u0002 aside to save them from \u0002{0}\u0002.", "hunter_discard": "Your target has died, so you may now pick a new one.", "wild_child_already_picked": "You have already picked your idol for this game.", "wild_child_success": "You have picked {0} to be your idol for this game.", @@ -726,7 +729,7 @@ "succubus_win": "Game over! The {0} {1} completely enthralled the village, making them officers in an ever-growing army set on spreading their {2} control and influence throughout the entire world.", "dullahan_die_totem": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002; however, {1}'s totem emits a brilliant flash of light, causing the attempt to miss.", "dullahan_die_angel": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002; however, a guardian angel was on duty and able to foil the attempt.", - "dullahan_die_bodyguard": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002; however, \u0002{2}\u0002, a bodyguard, sacrificed their life to protect them.", + "dullahan_die_bodyguard": "Sensing danger, \u0002{2}\u0002 shoves \u0002{1}\u0002 aside to save them from \u0002{0}\u0002.", "assassin_fail_blessed": "\u0002{0}\u0002 seems to be blessed, causing your assassination attempt to fail.", "dullahan_die_success": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002, killing them. The village mourns the loss of a{2} \u0002{3}\u0002.", "dullahan_die_success_noreveal": "Before dying, \u0002{0}\u0002 snaps a whip made of a human spine at \u0002{1}\u0002, killing them.", diff --git a/src/roles/angel.py b/src/roles/angel.py index 7bccdd2..cf43d8e 100644 --- a/src/roles/angel.py +++ b/src/roles/angel.py @@ -7,7 +7,7 @@ from collections import defaultdict import botconfig import src.settings as var from src.utilities import * -from src import debuglog, errlog, plog +from src import users, debuglog, errlog, plog from src.decorators import cmd, event_listener from src.messages import messages from src.events import Event @@ -277,8 +277,8 @@ def on_assassinate(evt, cli, var, nick, target, prot): for bg in var.ROLES["bodyguard"]: if GUARDED.get(bg) == target: cli.msg(botconfig.CHANNEL, messages[evt.params.message_prefix + "bodyguard"].format(nick, target, bg)) - evt.params.del_player(cli, bg, True, end_game=False, killer_role=evt.params.nickrole, deadlist=evt.params.deadlist, original=evt.params.original, ismain=False) - evt.data["pl"] = evt.params.refresh_pl(evt.data["pl"]) + # redirect the assassination to the bodyguard + evt.data["target"] = users._get(bg) # FIXME break @event_listener("begin_day") diff --git a/src/roles/dullahan.py b/src/roles/dullahan.py index 5b0268b..b3a65dc 100644 --- a/src/roles/dullahan.py +++ b/src/roles/dullahan.py @@ -70,25 +70,34 @@ def on_del_player(evt, cli, var, nick, mainrole, allroles, death_triggers): pl = evt.data["pl"] targets = TARGETS[users._get(nick)].intersection(users._get(x) for x in pl) # FIXME if targets: - target = random.choice(list(targets)).nick - prots = deque(var.ACTIVE_PROTECTIONS[target]) - aevt = Event("assassinate", {"pl": evt.data["pl"]}, + target = random.choice(list(targets)) + prots = deque(var.ACTIVE_PROTECTIONS[target.nick]) + aevt = Event("assassinate", {"pl": evt.data["pl"], "target": target}, del_player=evt.params.del_player, deadlist=evt.params.deadlist, original=evt.params.original, refresh_pl=evt.params.refresh_pl, message_prefix="dullahan_die_", + source="dullahan", + killer=nick, killer_mainrole=mainrole, killer_allroles=allroles, prots=prots) while len(prots) > 0: - # an event can read the current active protection and cancel the totem + # an event can read the current active protection and cancel or redirect the assassination # if it cancels, it is responsible for removing the protection from var.ACTIVE_PROTECTIONS # so that it cannot be used again (if the protection is meant to be usable once-only) - if not aevt.dispatch(cli, var, nick, target, prots[0]): + if not aevt.dispatch(cli, var, nick, target.nick, prots[0]): evt.data["pl"] = aevt.data["pl"] + if target is not aevt.data["target"]: + target = aevt.data["target"] + prots = deque(var.ACTIVE_PROTECTIONS[target.nick]) + aevt.params.prots = prots + continue return prots.popleft() + + target = target.nick # FIXME if var.ROLE_REVEAL in ("on", "team"): role = get_reveal_role(target) an = "n" if role.startswith(("a", "e", "i", "o", "u")) else "" diff --git a/src/roles/fallenangel.py b/src/roles/fallenangel.py index d1209dc..7194a70 100644 --- a/src/roles/fallenangel.py +++ b/src/roles/fallenangel.py @@ -44,7 +44,7 @@ def on_transition_day(evt, cli, var): def on_assassinate(evt, cli, var, nick, target, prot): # bypass all protection if FA is doing the killing # we do this by stopping propagation, meaning future events won't fire - if evt.params.nickrole == "fallen angel": + if "fallen angel" in evt.params.killer_allroles: evt.params.prots.clear() evt.stop_processing = True evt.prevent_default = True diff --git a/src/roles/madscientist.py b/src/roles/madscientist.py new file mode 100644 index 0000000..0f3533c --- /dev/null +++ b/src/roles/madscientist.py @@ -0,0 +1,162 @@ +import re +import random +import itertools +import math +from collections import defaultdict, deque + +import botconfig +import src.settings as var +from src.utilities import * +from src import channels, users, debuglog, errlog, plog +from src.decorators import command, event_listener +from src.messages import messages +from src.events import Event + +def _get_targets(var, pl, nick): + """Gets the mad scientist's targets. + + var - settings module + pl - list of alive players + nick - nick of the mad scientist""" + for index, user in enumerate(var.ALL_PLAYERS): + if user.nick == nick: # FIXME + break + + num_players = len(var.ALL_PLAYERS) + target1 = var.ALL_PLAYERS[index - 1] + target2 = var.ALL_PLAYERS[(index + 1) % num_players] + if num_players >= var.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS: + # determine left player + i = index + while True: + i = (i - 1) % num_players + if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == nick: + target1 = var.ALL_PLAYERS[i] + break + # determine right player + i = index + while True: + i = (i + 1) % num_players + if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == nick: + target2 = var.ALL_PLAYERS[i] + break + + return (target1, target2) + + +@event_listener("del_player") +def on_del_player(evt, cli, var, nick, mainrole, allroles, death_triggers): + if not death_triggers or "mad scientist" not in allroles: + return + + pl = evt.data["pl"] + target1, target2 = _get_targets(var, pl, nick) + + # apply protections (if applicable) + prots1 = deque(var.ACTIVE_PROTECTIONS[target1.nick]) + prots2 = deque(var.ACTIVE_PROTECTIONS[target2.nick]) + # for this event, we don't tell the event that the other side is dying + # this allows, e.g. a bodyguard and the person they are guarding to get splashed, + # and the bodyguard to still sacrifice themselves to guard the other person + aevt = Event("assassinate", {"pl": pl, "target": target1}, + del_player=evt.params.del_player, + deadlist=evt.params.deadlist, + original=evt.params.original, + refresh_pl=evt.params.refresh_pl, + message_prefix="mad_scientist_fail_", + source="mad scientist", + killer=nick, + killer_mainrole=mainrole, + killer_allroles=allroles, + prots=prots1) + while len(prots1) > 0: + # events may be able to cancel this kill + if not aevt.dispatch(cli, var, nick, target1.nick, prots1[0]): + pl = aevt.data["pl"] + if target1 is not aevt.data["target"]: + target1 = aevt.data["target"] + prots1 = deque(var.ACTIVE_PROTECTIONS[target1.nick]) + aevt.params.prots = prots1 + continue + break + prots1.popleft() + aevt.data["target"] = target2 + aevt.params.prots = prots2 + while len(prots2) > 0: + # events may be able to cancel this kill + if not aevt.dispatch(cli, var, nick, target2.nick, prots2[0]): + pl = aevt.data["pl"] + if target2 is not aevt.data["target"]: + target2 = aevt.data["target"] + prots2 = deque(var.ACTIVE_PROTECTIONS[target2.nick]) + aevt.params.prots = prots2 + continue + break + prots2.popleft() + + kill1 = target1.nick in pl and len(prots1) == 0 + kill2 = target2.nick in pl and len(prots2) == 0 and target1 is not target2 + + if kill1: + if kill2: + if var.ROLE_REVEAL in ("on", "team"): + r1 = get_reveal_role(target1.nick) + an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else "" + r2 = get_reveal_role(target2.nick) + an2 = "n" if r2.startswith(("a", "e", "i", "o", "u")) else "" + tmsg = messages["mad_scientist_kill"].format(nick, target1, an1, r1, target2, an2, r2) + else: + tmsg = messages["mad_scientist_kill_no_reveal"].format(nick, target1, target2) + cli.msg(botconfig.CHANNEL, tmsg) + debuglog(nick, "(mad scientist) KILL: {0} ({1}) - {2} ({3})".format(target1, get_role(target1.nick), target2, get_role(target2.nick))) + # here we DO want to tell that the other one is dying already so chained deaths don't mess things up + deadlist1 = evt.params.deadlist[:] + deadlist1.append(target2) + deadlist2 = evt.params.deadlist[:] + deadlist2.append(target1) + evt.params.del_player(cli, target1.nick, True, end_game=False, killer_role="mad scientist", deadlist=deadlist1, original=evt.params.original, ismain=False) + evt.params.del_player(cli, target2.nick, True, end_game=False, killer_role="mad scientist", deadlist=deadlist2, original=evt.params.original, ismain=False) + pl = evt.params.refresh_pl(pl) + else: + if var.ROLE_REVEAL in ("on", "team"): + r1 = get_reveal_role(target1.nick) + an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else "" + tmsg = messages["mad_scientist_kill_single"].format(nick, target1, an1, r1) + else: + tmsg = messages["mad_scientist_kill_single_no_reveal"].format(nick, target1) + cli.msg(botconfig.CHANNEL, tmsg) + debuglog(nick, "(mad scientist) KILL: {0} ({1})".format(target1, get_role(target1.nick))) + evt.params.del_player(cli, target1.nick, True, end_game=False, killer_role="mad scientist", deadlist=evt.params.deadlist, original=evt.params.original, ismain=False) + pl = evt.params.refresh_pl(pl) + else: + if kill2: + if var.ROLE_REVEAL in ("on", "team"): + r2 = get_reveal_role(target2.nick) + an2 = "n" if r2.startswith(("a", "e", "i", "o", "u")) else "" + tmsg = messages["mad_scientist_kill_single"].format(nick, target2, an2, r2) + else: + tmsg = messages["mad_scientist_kill_single_no_reveal"].format(nick, target2) + cli.msg(botconfig.CHANNEL, tmsg) + debuglog(nick, "(mad scientist) KILL: {0} ({1})".format(target2, get_role(target2.nick))) + evt.params.del_player(cli, target2.nick, True, end_game=False, killer_role="mad scientist", deadlist=evt.params.deadlist, original=evt.params.original, ismain=False) + pl = evt.params.refresh_pl(pl) + else: + tmsg = messages["mad_scientist_fail"].format(nick) + cli.msg(botconfig.CHANNEL, tmsg) + debuglog(nick, "(mad scientist) KILL FAIL") + + evt.data["pl"] = pl + +@event_listener("transition_night_end", priority=2) +def on_transition_night_end(evt, cli, var): + for ms in var.ROLES["mad scientist"]: + pl = list_players() + target1, target2 = _get_targets(var, pl, ms) + + if ms in var.PLAYERS and not is_user_simple(ms): + pm(cli, ms, messages["mad_scientist_notify"].format(target1, target2)) + else: + pm(cli, ms, messages["mad_scientist_simple"].format(target1, target2)) + + +# vim: set sw=4 expandtab: diff --git a/src/wolfgame.py b/src/wolfgame.py index 5b67242..f1f8b9f 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -2487,13 +2487,16 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death target = var.TARGETED[nick] del var.TARGETED[nick] if target is not None and target in pl: + targuser = users._get(target) # FIXME prots = deque(var.ACTIVE_PROTECTIONS[target]) - aevt = Event("assassinate", {"pl": pl}, + aevt = Event("assassinate", {"pl": pl, "target": targuser}, del_player=del_player, deadlist=deadlist, original=original, refresh_pl=refresh_pl, message_prefix="assassin_fail_", + source="assassin", + killer=nick, killer_mainrole=nickrole, killer_allroles=allroles, prots=prots) @@ -2503,6 +2506,12 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death # so that it cannot be used again (if the protection is meant to be usable once-only) if not aevt.dispatch(cli, var, nick, target, prots[0]): pl = aevt.data["pl"] + if targuser is not aevt.data["target"]: + targuser = aevt.data["target"] + target = targuser.nick + prots = deque(var.ACTIVE_PROTECTIONS[target]) + aevt.params.prots = prots + continue break prots.popleft() if len(prots) == 0: @@ -2513,8 +2522,8 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death else: message = messages["assassin_success_no_reveal"].format(nick, target) cli.msg(botconfig.CHANNEL, message) - debuglog("{0} ({1}) ASSASSINATE: {2} ({3})".format(nick, nickrole, target, get_role(target))) - del_player(cli, target, True, end_game = False, killer_role = nickrole, deadlist = deadlist, original = original, ismain = False) + debuglog("{0} (assassin) ASSASSINATE: {1} ({2})".format(nick, target, get_role(target))) + del_player(cli, target, True, end_game=False, killer_role=nickrole, deadlist=deadlist, original=original, ismain=False) pl = refresh_pl(pl) if nickrole == "time lord": if "DAY_TIME_LIMIT" not in var.ORIGINAL_SETTINGS: @@ -2567,86 +2576,6 @@ def del_player(cli, nick, forced_death=False, devoice=True, end_game=True, death debuglog(nick, "(time lord) TRIGGER") - if nickrole == "mad scientist": - # kills the 2 players adjacent to them in the original players listing (in order of !joining) - # if those players are already dead, nothing happens - for index, user in enumerate(var.ALL_PLAYERS): - if user.nick == nick: # FIXME - break - targets = [] - target1 = var.ALL_PLAYERS[index - 1] - target2 = var.ALL_PLAYERS[index + 1 if index < len(var.ALL_PLAYERS) - 1 else 0] - if len(var.ALL_PLAYERS) >= var.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS: - # determine left player - i = index - while True: - i -= 1 - if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == nick: - target1 = var.ALL_PLAYERS[i] - break - # determine right player - i = index - while True: - i += 1 - if i >= len(var.ALL_PLAYERS): - i = 0 - if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == nick: - target2 = var.ALL_PLAYERS[i] - break - - # do not kill blessed players, they had a premonition to step out of the way before the chemicals hit - if target1.nick in var.ROLES["blessed villager"]: - target1 = None - if target2.nick in var.ROLES["blessed villager"]: - target2 = None - - if target1.nick in pl: - if target2.nick in pl and target1 is not target2: - if var.ROLE_REVEAL in ("on", "team"): - r1 = get_reveal_role(target1.nick) - an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else "" - r2 = get_reveal_role(target2.nick) - an2 = "n" if r2.startswith(("a", "e", "i", "o", "u")) else "" - tmsg = messages["mad_scientist_kill"].format(nick, target1, an1, r1, target2, an2, r2) - else: - tmsg = messages["mad_scientist_kill_no_reveal"].format(nick, target1, target2) - cli.msg(botconfig.CHANNEL, tmsg) - debuglog(nick, "(mad scientist) KILL: {0} ({1}) - {2} ({3})".format(target1, get_role(target1.nick), target2, get_role(target2.nick))) - deadlist1 = copy.copy(deadlist) - deadlist1.append(target2) - deadlist2 = copy.copy(deadlist) - deadlist2.append(target1) - del_player(cli, target1.nick, True, end_game = False, killer_role = "mad scientist", deadlist = deadlist1, original = original, ismain = False) - del_player(cli, target2.nick, True, end_game = False, killer_role = "mad scientist", deadlist = deadlist2, original = original, ismain = False) - pl = refresh_pl(pl) - else: - if var.ROLE_REVEAL in ("on", "team"): - r1 = get_reveal_role(target1.nick) - an1 = "n" if r1.startswith(("a", "e", "i", "o", "u")) else "" - tmsg = messages["mad_scientist_kill_single"].format(nick, target1, an1, r1) - else: - tmsg = messages["mad_scientist_kill_single_no_reveal"].format(nick, target1) - cli.msg(botconfig.CHANNEL, tmsg) - debuglog(nick, "(mad scientist) KILL: {0} ({1})".format(target1, get_role(target1.nick))) - del_player(cli, target1.nick, True, end_game = False, killer_role = "mad scientist", deadlist = deadlist, original = original, ismain = False) - pl = refresh_pl(pl) - else: - if target2.nick in pl: - if var.ROLE_REVEAL in ("on", "team"): - r2 = get_reveal_role(target2.nick) - an2 = "n" if r2.startswith(("a", "e", "i", "o", "u")) else "" - tmsg = messages["mad_scientist_kill_single"].format(nick, target2, an2, r2) - else: - tmsg = messages["mad_scientist_kill_single_no_reveal"].format(nick, target2) - cli.msg(botconfig.CHANNEL, tmsg) - debuglog(nick, "(mad scientist) KILL: {0} ({1})".format(target2, get_role(target2.nick))) - del_player(cli, target2.nick, True, end_game = False, killer_role = "mad scientist", deadlist = deadlist, original = original, ismain = False) - pl = refresh_pl(pl) - else: - tmsg = messages["mad_scientist_fail"].format(nick) - cli.msg(botconfig.CHANNEL, tmsg) - debuglog(nick, "(mad scientist) KILL FAIL") - pl = refresh_pl(pl) # i herd u liek parameters evt_death_triggers = death_triggers and var.PHASE in var.GAME_PHASES @@ -5331,36 +5260,6 @@ def transition_night(cli): else: pm(cli, drunk, messages["drunk_simple"]) - for ms in var.ROLES["mad scientist"]: - pl = ps[:] - for index, user in enumerate(var.ALL_PLAYERS): - if user.nick == ms: - break - targets = [] - target1 = var.ALL_PLAYERS[index - 1] - target2 = var.ALL_PLAYERS[index + 1 if index < len(var.ALL_PLAYERS) - 1 else 0] - if len(var.ALL_PLAYERS) >= var.MAD_SCIENTIST_SKIPS_DEAD_PLAYERS: - # determine left player - i = index - while True: - i -= 1 - if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == ms: - target1 = var.ALL_PLAYERS[i] - break - # determine right player - i = index - while True: - i += 1 - if i >= len(var.ALL_PLAYERS): - i = 0 - if var.ALL_PLAYERS[i].nick in pl or var.ALL_PLAYERS[i].nick == ms: - target2 = var.ALL_PLAYERS[i] - break - if ms in var.PLAYERS and not is_user_simple(ms): - pm(cli, ms, messages["mad_scientist_notify"].format(target1, target2)) - else: - pm(cli, ms, messages["mad_scientist_simple"].format(target1, target2)) - for doctor in var.ROLES["doctor"]: if doctor in var.DOCTORS and var.DOCTORS[doctor] > 0: # has immunizations remaining pl = ps[:]