From 38786daa7e8f787aa041c800b79fc435ccb7a55d Mon Sep 17 00:00:00 2001 From: Gurge44 Date: Mon, 8 Jan 2024 19:46:44 +0100 Subject: [PATCH] Add Kamikaze, Fix Disperser, Optimize Pet Suffix --- Modules/CustomRolesHelper.cs | 4 + Modules/DoubleTrigger.cs | 9 +- Modules/ExtendedPlayerControl.cs | 6 +- .../PlayerGameOptionsSender.cs | 4 + Modules/GameState.cs | 1 + Modules/OptionHolder.cs | 4 + Modules/RPC.cs | 14 ++ Patches/IntroPatch.cs | 25 ++-- Patches/PlayerContorolPatch.cs | 20 ++- Patches/PlayerJoinAndLeftPatch.cs | 10 +- Patches/onGameStartedPatch.cs | 4 + Resources/Lang/en_US.json | 12 +- Roles/Crewmate/Divinator.cs | 2 + Roles/Crewmate/Merchant.cs | 8 +- Roles/Impostor/Hacker.cs | 2 +- Roles/Impostor/Kamikaze.cs | 121 ++++++++++++++++++ Roles/Impostor/Kidnapper.cs | 29 +++++ main.cs | 2 + 18 files changed, 249 insertions(+), 28 deletions(-) create mode 100644 Roles/Impostor/Kamikaze.cs create mode 100644 Roles/Impostor/Kidnapper.cs diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 28e542659..e92b2638a 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -52,6 +52,8 @@ internal static class CustomRolesHelper CustomRoles.SpeedBooster => CustomRoles.Crewmate, CustomRoles.Dictator => CustomRoles.Crewmate, CustomRoles.Inhibitor => CustomRoles.Impostor, + CustomRoles.Kidnapper => CustomRoles.Shapeshifter, + CustomRoles.Kamikaze => CustomRoles.Impostor, CustomRoles.Librarian => CustomRoles.Shapeshifter, CustomRoles.Cantankerous => CustomRoles.Impostor, CustomRoles.YinYanger => CustomRoles.Impostor, @@ -754,6 +756,8 @@ internal static class CustomRolesHelper CustomRoles.SerialKiller or CustomRoles.Underdog or CustomRoles.Inhibitor or + CustomRoles.Kidnapper or + CustomRoles.Kamikaze or CustomRoles.Librarian or CustomRoles.Cantankerous or CustomRoles.Duellist or diff --git a/Modules/DoubleTrigger.cs b/Modules/DoubleTrigger.cs index 631bd9013..b40f2939d 100644 --- a/Modules/DoubleTrigger.cs +++ b/Modules/DoubleTrigger.cs @@ -22,10 +22,7 @@ static class DoubleTrigger { PlayerIdList.Add(killer.PlayerId); } - public static bool CanDoubleTrigger(this PlayerControl killer) - { - return PlayerIdList.Contains(killer.PlayerId); - } + public static bool CanDoubleTrigger(this PlayerControl killer) => PlayerIdList.Contains(killer.PlayerId); /// /// Checks for whether the killer pressed their kill button twice on the same player @@ -41,7 +38,7 @@ static class DoubleTrigger { if (FirstTriggerTarget[killer.PlayerId] != target.PlayerId) { - //2回目がターゲットずれてたら最初の相手にシングルアクション + // If the second target is off target, single action on the first opponent. return false; } Logger.Info($"{killer.name} DoDoubleAction", "DoubleTrigger"); @@ -50,7 +47,7 @@ static class DoubleTrigger if (doAction) FirstTriggerAction.Remove(killer.PlayerId); return true; } - //シングルアクション時はキル間隔を無視 + // Ignore kill interval during single action CheckMurderPatch.TimeSinceLastKill.Remove(killer.PlayerId); FirstTriggerTimer.Add(killer.PlayerId, 1f); FirstTriggerTarget.Add(killer.PlayerId, target.PlayerId); diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index c642a7cb9..10c5c3960 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -497,6 +497,8 @@ static class ExtendedPlayerControl CustomRoles.RiftMaker => true, CustomRoles.Hitman => true, CustomRoles.Inhibitor => true, + CustomRoles.Kidnapper => true, + CustomRoles.Kamikaze => true, CustomRoles.Cantankerous => true, CustomRoles.YinYanger => true, CustomRoles.Duellist => true, @@ -588,7 +590,7 @@ static class ExtendedPlayerControl CustomRoles.PlagueBearer => true, CustomRoles.Pestilence => true, - _ => pc.Is(CustomRoleTypes.Impostor), + _ => pc.Is(CustomRoleTypes.Impostor) || pc.GetCustomRole().IsNK() || pc.GetCustomRole().IsTasklessCrewmate(), }; } public static bool CanUseKillButton(this PlayerControl pc) @@ -627,6 +629,8 @@ static class ExtendedPlayerControl CustomRoles.YinYanger => pc.IsAlive(), CustomRoles.Librarian => pc.IsAlive() && Librarian.CanUseKillButton(pc), CustomRoles.Cantankerous => pc.IsAlive() && Cantankerous.CanUseKillButton(pc.PlayerId), + CustomRoles.Kidnapper => pc.IsAlive(), + CustomRoles.Kamikaze => pc.IsAlive(), CustomRoles.Inhibitor => !IsActive(SystemTypes.Electrical) && !IsActive(SystemTypes.Comms) && !IsActive(SystemTypes.MushroomMixupSabotage) && !IsActive(SystemTypes.Laboratory) && !IsActive(SystemTypes.LifeSupp) && !IsActive(SystemTypes.Reactor) && !IsActive(SystemTypes.HeliSabotage), CustomRoles.Saboteur => IsActive(SystemTypes.Electrical) || IsActive(SystemTypes.Comms) || IsActive(SystemTypes.MushroomMixupSabotage) || IsActive(SystemTypes.Laboratory) || IsActive(SystemTypes.LifeSupp) || IsActive(SystemTypes.Reactor) || IsActive(SystemTypes.HeliSabotage), CustomRoles.Sniper => Sniper.CanUseKillButton(pc), diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index 1cc5f00c6..21206ac14 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -652,6 +652,10 @@ public class PlayerGameOptionsSender(PlayerControl player) : GameOptionsSender AURoleOptions.EngineerCooldown = Sentinel.PatrolCooldown.GetFloat(); AURoleOptions.EngineerInVentMaxTime = 1f; break; + case CustomRoles.Kidnapper: + AURoleOptions.ShapeshifterCooldown = Kidnapper.SSCD.GetFloat(); + AURoleOptions.ShapeshifterDuration = 1f; + break; case CustomRoles.Spiritcaller: opt.SetVision(Spiritcaller.ImpostorVision.GetBool()); break; diff --git a/Modules/GameState.cs b/Modules/GameState.cs index 7971e2200..44ace5105 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -250,6 +250,7 @@ public class PlayerState(byte playerId) Demolished, YinYanged, Hack, + Kamikazed, etc = -1, } diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index 451950f7a..4ae79e0c3 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -1225,6 +1225,10 @@ public static class Options InhibitorCDAfterMeetings = FloatOptionItem.Create(1511, "AfterMeetingKillCooldown", new(0f, 180f, 2.5f), 22.5f, TabGroup.ImpostorRoles, false) .SetParent(CustomRoleSpawnChances[CustomRoles.Inhibitor]) .SetValueFormat(OptionFormat.Seconds); + RoleLoadingText = "Impostor roles\nKamikaze"; + Kamikaze.SetupCustomOption(); + RoleLoadingText = "Impostor roles\nKidnapper"; + Kidnapper.SetupCustomOption(); RoleLoadingText = "Impostor roles\nLibrarian"; Librarian.SetupCustomOption(); RoleLoadingText = "Impostor roles\nCantankerous"; diff --git a/Modules/RPC.cs b/Modules/RPC.cs index e2161d5ff..9a1cbeaad 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -58,6 +58,8 @@ enum CustomRPC //Roles SetDrawPlayer, + SyncKamikazeLimit, + KamikazeAddTarget, SyncMycologist, SyncBubble, AddTornado, @@ -593,6 +595,12 @@ internal class RPCHandlerPatch case CustomRPC.SyncBubble: Bubble.ReceiveRPC(reader); break; + case CustomRPC.KamikazeAddTarget: + Kamikaze.ReceiveRPCAddTarget(reader); + break; + case CustomRPC.SyncKamikazeLimit: + Kamikaze.ReceiveRPCSyncLimit(reader); + break; case CustomRPC.SetCurrentDousingTarget: byte arsonistId = reader.ReadByte(); byte dousingTargetId = reader.ReadByte(); @@ -1266,6 +1274,12 @@ internal static class RPC case CustomRoles.EvilDiviner: EvilDiviner.Add(targetId); break; + case CustomRoles.Kamikaze: + Kamikaze.Add(targetId); + break; + case CustomRoles.Disperser: + Disperser.Add(targetId); + break; case CustomRoles.Medic: Medic.Add(targetId); break; diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index e8acf1808..33849f66d 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -317,23 +317,24 @@ class BeginCrewmatePatch CustomRoles.Disperser => ShipStatus.Instance.VentMoveSounds.FirstOrDefault(), - _ => PlayerControl.LocalPlayer.Is(RoleTypes.Impostor) ? PlayerControl.LocalPlayer.Is(RoleTypes.Shapeshifter) ? GetIntroSound(RoleTypes.Shapeshifter) : GetIntroSound(RoleTypes.Impostor) : GetIntroSound(RoleTypes.Crewmate), + _ => role.GetCustomRoleTypes() switch + { + CustomRoleTypes.Impostor => GetIntroSound(RoleTypes.Impostor), + CustomRoleTypes.Crewmate => GetIntroSound(RoleTypes.Crewmate), + CustomRoleTypes.Neutral => GetIntroSound(RoleTypes.Shapeshifter), + _ => GetIntroSound(RoleTypes.Crewmate), + } }; } catch (Exception ex) { - switch (role.GetCustomRoleTypes()) + PlayerControl.LocalPlayer.Data.Role.IntroSound = role.GetCustomRoleTypes() switch { - case CustomRoleTypes.Impostor: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Impostor); - break; - case CustomRoleTypes.Crewmate: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Crewmate); - break; - case CustomRoleTypes.Neutral: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Shapeshifter); - break; - } + CustomRoleTypes.Impostor => GetIntroSound(RoleTypes.Impostor), + CustomRoleTypes.Crewmate => GetIntroSound(RoleTypes.Crewmate), + CustomRoleTypes.Neutral => GetIntroSound(RoleTypes.Shapeshifter), + _ => GetIntroSound(RoleTypes.Crewmate), + }; Logger.Warn($"Could not set intro sound\n{ex}", "IntroSound"); } diff --git a/Patches/PlayerContorolPatch.cs b/Patches/PlayerContorolPatch.cs index 6b900f36f..91ea1c615 100644 --- a/Patches/PlayerContorolPatch.cs +++ b/Patches/PlayerContorolPatch.cs @@ -217,6 +217,9 @@ class CheckMurderPatch case CustomRoles.BountyHunter: BountyHunter.OnCheckMurder(killer, target); break; + case CustomRoles.Kamikaze: + if (!Kamikaze.OnCheckMurder(killer, target)) return false; + break; case CustomRoles.Reckless: Reckless.OnCheckMurder(killer); break; @@ -1210,6 +1213,7 @@ class MurderPlayerPatch break; case CustomRoles.Hacker: Hacker.HackLimit[killer.PlayerId] += Hacker.HackerAbilityUseGainWithEachKill.GetFloat(); + Hacker.SendRPC(killer.PlayerId); break; case CustomRoles.Camouflager: Camouflager.CamoLimit[killer.PlayerId] += Camouflager.CamoAbilityUseGainWithEachKill.GetFloat(); @@ -1235,6 +1239,10 @@ class MurderPlayerPatch case CustomRoles.Twister: Twister.TwistLimit[killer.PlayerId] += Twister.TwisterAbilityUseGainWithEachKill.GetFloat(); break; + case CustomRoles.Kamikaze: + Kamikaze.MarkLimit[killer.PlayerId] += Kamikaze.KamikazeAbilityUseGainWithEachKill.GetFloat(); + Kamikaze.SendRPCSyncLimit(killer.PlayerId); + break; } if (killer.Is(CustomRoles.TicketsStealer) && killer.PlayerId != target.PlayerId) @@ -1367,6 +1375,10 @@ class ShapeshiftPatch EvilTracker.OnShapeshift(shapeshifter, target, shapeshifting); isSSneeded = false; break; + case CustomRoles.Kidnapper: + Kidnapper.OnShapeshift(shapeshifter, target); + isSSneeded = false; + break; case CustomRoles.RiftMaker: RiftMaker.OnShapeshift(shapeshifter, shapeshifting); isSSneeded = false; @@ -2011,6 +2023,7 @@ class FixedUpdatePatch private static int LevelKickBufferTime = 10; private static readonly Dictionary BufferTime = []; private static readonly Dictionary DeadBufferTime = []; + private static readonly Dictionary LastUpdate = []; public static async void Postfix(PlayerControl __instance) { @@ -2137,6 +2150,9 @@ class FixedUpdatePatch case CustomRoles.BountyHunter: BountyHunter.FixedUpdate(player); break; + case CustomRoles.Kamikaze when !lowLoad: + Kamikaze.OnFixedUpdate(); + break; case CustomRoles.Glitch when !lowLoad: Glitch.UpdateHackCooldown(player); break; @@ -2233,7 +2249,8 @@ class FixedUpdatePatch } } - if (!lowLoad && Options.UsePets.GetBool() && GameStates.IsInTask) + long now = GetTimeStamp(); + if (!lowLoad && Options.UsePets.GetBool() && GameStates.IsInTask && (!LastUpdate.TryGetValue(playerId, out var lastPetNotify) || lastPetNotify < now)) { if (Main.AbilityCD.TryGetValue(playerId, out var timer)) { @@ -2242,6 +2259,7 @@ class FixedUpdatePatch Main.AbilityCD.Remove(playerId); } if (!player.IsModClient()) NotifyRoles(SpecifySeer: player, SpecifyTarget: player); + LastUpdate[playerId] = now; } } diff --git a/Patches/PlayerJoinAndLeftPatch.cs b/Patches/PlayerJoinAndLeftPatch.cs index 77c8a2b1c..e24137c7c 100644 --- a/Patches/PlayerJoinAndLeftPatch.cs +++ b/Patches/PlayerJoinAndLeftPatch.cs @@ -141,10 +141,14 @@ class OnPlayerLeftPatch Spiritualist.RemoveTarget(); if (data.Character.PlayerId == Postman.Target) Postman.SetNewTarget(); - if (Main.PlayerStates[data.Character.PlayerId].deathReason == PlayerState.DeathReason.etc) //死因が設定されていなかったら + PlayerState state = Main.PlayerStates[data.Character.PlayerId]; + if (state.deathReason == PlayerState.DeathReason.etc) // If no cause of death was established { - Main.PlayerStates[data.Character.PlayerId].deathReason = PlayerState.DeathReason.Disconnected; - Main.PlayerStates[data.Character.PlayerId].SetDead(); + state.deathReason = PlayerState.DeathReason.Disconnected; + } + if (!state.IsDead) + { + state.SetDead(); } NameNotifyManager.Notice.Remove(data.Character.PlayerId); AntiBlackout.OnDisconnect(data.Character.Data); diff --git a/Patches/onGameStartedPatch.cs b/Patches/onGameStartedPatch.cs index 6a1465ada..99cc0d106 100644 --- a/Patches/onGameStartedPatch.cs +++ b/Patches/onGameStartedPatch.cs @@ -196,6 +196,7 @@ internal class ChangeRoleSettings BountyHunter.Init(); SerialKiller.Init(); EvilDiviner.Init(); + Kamikaze.Init(); FireWorks.Init(); NiceSwapper.Init(); Pickpocket.Init(); @@ -804,6 +805,9 @@ internal class SelectRolesPatch case CustomRoles.EvilDiviner: EvilDiviner.Add(pc.PlayerId); break; + case CustomRoles.Kamikaze: + Kamikaze.Add(pc.PlayerId); + break; case CustomRoles.Ritualist: Ritualist.Add(pc.PlayerId); break; diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 9bde6d439..6ae13b9b8 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -1604,6 +1604,7 @@ "DeathReason.Jinx": "Jinxed", "DeathReason.Demolished": "Demolished", "DeathReason.YinYanged": "Yin Yanged", + "DeathReason.Kamikazed": "Kamikazed", "DeathReason.etc": "Other", "DeathReason.Hack": "Hacked", "DeathReason.Curse": "Cursed", @@ -2324,7 +2325,7 @@ "DivinatorCheck.Result4": "{0} is either Pestilence, Electric, Plaguebearer, Donut Delivery, Observer, Blood Knight, Guardian, or Wildling", "DivinatorCheck.Result5": "{0} is either Bard, Sentinel, Ricochet, Juggernaut, Security Guard, Arrogance, Killing Machine, or Butcher", "DivinatorCheck.Result6": "{0} is either Coroner, Mole, Stealth, Evil Tracker, Mortician, Tracefinder, Romantic, or Tracker", - "DivinatorCheck.Result7": "{0} is either Bodyguard, Bomber, Lookout, Vengeful Romantic, Ruthless Romantic, Fireworker, or Nuker", + "DivinatorCheck.Result7": "{0} is either Bodyguard, Bomber, Lookout, Vengeful Romantic, Ruthless Romantic, Kidnapper, Fireworker, or Nuker", "DivinatorCheck.Result8": "{0} is either Bounty Hunter, Sprayer, Mafioso, Detective, Hater, Cleaner, Medusa, or Psychic", "DivinatorCheck.Result9": "{0} is either Convict, Penguin, Hookshot, Executioner, Lawyer, Snitch, Disperser, or Doctor", "DivinatorCheck.Result10": "{0} is either Councillor, Escort, Bubble, Shiftguard, Dictator, Judge, Cleanser, or Cursed Wolf", @@ -2335,7 +2336,7 @@ "DivinatorCheck.Result15": "{0} is either Infectious, Detour, Virus, Agitator, Monarch, Revolutionist, Express, or Cultist", "DivinatorCheck.Result16": "{0} is either Innocent, Gaulois, Sapper, Refugee, Bandit, Inhibitor, Mechanic, or Saboteur", "DivinatorCheck.Result17": "{0} is either Medic, Vector, Analyzer, RiftMaker, Jester, The Glitch, Lurker, or Sunnyboy", - "DivinatorCheck.Result18": "{0} is either Nemesis, Hitman, Gangster, Glitch, Luckey, or Underdog", + "DivinatorCheck.Result18": "{0} is either Nemesis, Hitman, Gangster, Glitch, Kamikaze, Luckey, or Underdog", "DivinatorCheck.Result19": "{0} is either Evil Guesser, Nice Guesser, Blackmailer, Eclipse, Reckless, Stalker, Camouflager, or Chameleon", "DivinatorCheck.Result20": "{0} is either Jackal, Sidekick, Duellist, WeaponMaster, Maverick, Opportunist, Pursuer, or Provocateur", "DivinatorCheck.Result21": "{0} is either Poisoner, Magician, Yin Yanger, Vampire, Pacifist, Head Hunter, Soul Catcher, or Traitor", @@ -2395,6 +2396,13 @@ "pet_UFO": "UFO", "pet_D2WormPet": "Worm", "pet_RANDOM_FOR_EVERYONE": "Random", + "Kidnapper": "Kidnapper", + "KidnapperInfo": "TP Players to you by shifting into them", + "KidnapperInfoLong": "(Impostors):\\nAs the Kidnapper, when you shapeshift into someone, they teleport to you.", + "Kamikaze": "Kamikaze", + "KamikazeInfo": "Mark Players That Die With You", + "KamikazeInfoLong": "(Impostors):\\nAs the Kamikaze, you can mark players by clicking your kill button once on them. When you die, all players that you marked will also die. You can kill normally by double clicking your kill button on someone.", + "KamikazeMarkCD": "Cooldown After Marking", "Jailor": "Jailor", "JailorInfo": "Lock up your target in Jail", "JailorInfoLong": "(Crewmates):\\nAs the Jailor, use your kill button to lock a player in jail. Jailed players cannot be guessed or judged and jailed players can only guess Jailor.", diff --git a/Roles/Crewmate/Divinator.cs b/Roles/Crewmate/Divinator.cs index e3126b411..8dc2a440e 100644 --- a/Roles/Crewmate/Divinator.cs +++ b/Roles/Crewmate/Divinator.cs @@ -167,6 +167,7 @@ public static class Divinator CustomRoles.Lookout or CustomRoles.RuthlessRomantic or CustomRoles.VengefulRomantic or + CustomRoles.Kidnapper or CustomRoles.Nuker => "Result7", @@ -278,6 +279,7 @@ public static class Divinator CustomRoles.Doppelganger or CustomRoles.Hitman or CustomRoles.Luckey or + CustomRoles.Kamikaze or CustomRoles.Underdog => "Result18", diff --git a/Roles/Crewmate/Merchant.cs b/Roles/Crewmate/Merchant.cs index 6b950561d..e1e5ff05c 100644 --- a/Roles/Crewmate/Merchant.cs +++ b/Roles/Crewmate/Merchant.cs @@ -80,6 +80,7 @@ namespace TOHE.Roles.Crewmate private static OptionItem OptionCanSellExperimental; private static OptionItem OptionSellOnlyHarmfulToEvil; private static OptionItem OptionSellOnlyHelpfulToCrew; + private static OptionItem OptionGivesAllMoneyOnBribe; private static int GetCurrentAmountOfMoney(byte playerId) => (addonsSold[playerId] * OptionMoneyPerSell.GetInt()) - (bribedKiller[playerId].Count * OptionMoneyRequiredToBribe.GetInt()); @@ -101,8 +102,9 @@ namespace TOHE.Roles.Crewmate OptionSellOnlyEnabledAddons = BooleanOptionItem.Create(Id + 16, "MerchantSellOnlyEnabledAddons", false, TabGroup.CrewmateRoles, false).SetParent(CustomRoleSpawnChances[CustomRoles.Merchant]); OptionSellOnlyHarmfulToEvil = BooleanOptionItem.Create(Id + 14, "MerchantSellHarmfulToEvil", false, TabGroup.CrewmateRoles, false).SetParent(CustomRoleSpawnChances[CustomRoles.Merchant]); OptionSellOnlyHelpfulToCrew = BooleanOptionItem.Create(Id + 15, "MerchantSellHelpfulToCrew", false, TabGroup.CrewmateRoles, false).SetParent(CustomRoleSpawnChances[CustomRoles.Merchant]); + OptionGivesAllMoneyOnBribe = BooleanOptionItem.Create(Id + 17, "MerchantGivesAllMoneyOnBribe", false, TabGroup.CrewmateRoles, false).SetParent(CustomRoleSpawnChances[CustomRoles.Merchant]); - OverrideTasksData.Create(Id + 17, TabGroup.CrewmateRoles, CustomRoles.Merchant); + OverrideTasksData.Create(Id + 18, TabGroup.CrewmateRoles, CustomRoles.Merchant); } public static void Init() { @@ -204,7 +206,7 @@ namespace TOHE.Roles.Crewmate return false; } - if (bribedKiller[target.PlayerId].Contains(killer.PlayerId)) + if (IsBribedKiller(killer, target)) { NotifyBribery(killer, target); return true; @@ -224,6 +226,8 @@ namespace TOHE.Roles.Crewmate private static void NotifyBribery(PlayerControl killer, PlayerControl target) { + if (OptionGivesAllMoneyOnBribe.GetBool()) addonsSold[target.PlayerId] = 0; + killer.Notify(Utils.ColorString(Utils.GetRoleColor(CustomRoles.Merchant), GetString("BribedByMerchant"))); if (OptionNotifyBribery.GetBool()) diff --git a/Roles/Impostor/Hacker.cs b/Roles/Impostor/Hacker.cs index 08882ea60..bfa87f691 100644 --- a/Roles/Impostor/Hacker.cs +++ b/Roles/Impostor/Hacker.cs @@ -42,7 +42,7 @@ public static class Hacker HackLimit.TryAdd(playerId, HackLimitOpt.GetInt()); } public static bool IsEnable => playerIdList.Count > 0; - private static void SendRPC(byte playerId) + public static void SendRPC(byte playerId) { if (!IsEnable || !Utils.DoRPC) return; MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetHackerHackLimit, SendOption.Reliable, -1); diff --git a/Roles/Impostor/Kamikaze.cs b/Roles/Impostor/Kamikaze.cs new file mode 100644 index 000000000..0024ef53a --- /dev/null +++ b/Roles/Impostor/Kamikaze.cs @@ -0,0 +1,121 @@ +using Epic.OnlineServices; +using Hazel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static TOHE.Options; +using static TOHE.Translator; +using static TOHE.Utils; + +namespace TOHE.Roles.Impostor +{ + internal class Kamikaze + { + private static int Id => 643310; + public static bool IsEnable = false; + + private static readonly Dictionary> MarkedPlayers = []; + public static readonly Dictionary MarkLimit = []; + + private static OptionItem MarkCD; + private static OptionItem KamikazeLimitOpt; + public static OptionItem KamikazeAbilityUseGainWithEachKill; + + public static void SetupCustomOption() + { + SetupRoleOptions(Id, TabGroup.ImpostorRoles, CustomRoles.Kamikaze); + MarkCD = FloatOptionItem.Create(Id + 2, "KamikazeMarkCD", new(0f, 180f, 2.5f), 30f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Kamikaze]) + .SetValueFormat(OptionFormat.Seconds); + KamikazeLimitOpt = IntegerOptionItem.Create(Id + 5, "AbilityUseLimit", new(0, 5, 1), 1, TabGroup.ImpostorRoles, false).SetParent(CustomRoleSpawnChances[CustomRoles.Kamikaze]) + .SetValueFormat(OptionFormat.Times); + KamikazeAbilityUseGainWithEachKill = FloatOptionItem.Create(Id + 6, "AbilityUseGainWithEachKill", new(0f, 5f, 0.1f), 0.5f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Kamikaze]) + .SetValueFormat(OptionFormat.Times); + } + + public static void Init() + { + MarkedPlayers.Clear(); + MarkLimit.Clear(); + IsEnable = false; + } + + public static void Add(byte playerId) + { + MarkedPlayers[playerId] = []; + MarkLimit[playerId] = KamikazeLimitOpt.GetInt(); + IsEnable = true; + } + + public static void SendRPCSyncLimit(byte playerId) + { + if (!IsEnable || !DoRPC || playerId == 0) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncKamikazeLimit, SendOption.Reliable, -1); + writer.Write(playerId); + writer.Write(MarkLimit[playerId]); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + + private static void SendRPCAddTarget(byte kamikazeId, byte targetId) + { + if (!IsEnable || !DoRPC || kamikazeId == 0) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.KamikazeAddTarget, SendOption.Reliable, -1); + writer.Write(kamikazeId); + writer.Write(targetId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + + public static void ReceiveRPCSyncLimit(MessageReader reader) + { + byte playerId = reader.ReadByte(); + float limit = reader.ReadSingle(); + MarkLimit[playerId] = limit; + } + + public static void ReceiveRPCAddTarget(MessageReader reader) + { + byte playerId = reader.ReadByte(); + byte targetId = reader.ReadByte(); + MarkedPlayers[playerId].Add(targetId); + } + + public static bool OnCheckMurder(PlayerControl killer, PlayerControl target) + { + if (killer == null || target == null) return false; + if (MarkLimit.TryGetValue(killer.PlayerId, out var limit) && limit < 1) return true; + return killer.CheckDoubleTrigger(target, () => + { + MarkedPlayers[killer.PlayerId].Add(target.PlayerId); + SendRPCAddTarget(killer.PlayerId, target.PlayerId); + killer.SetKillCooldown(MarkCD.GetFloat()); + MarkLimit[killer.PlayerId]--; + SendRPCSyncLimit(killer.PlayerId); + }); + } + + public static void OnFixedUpdate() + { + if (!IsEnable) return; + + foreach (var kvp in MarkedPlayers) + { + var kamikazePc = GetPlayerById(kvp.Key); + if (kamikazePc.IsAlive()) continue; + + foreach (var id in kvp.Value) + { + var victim = GetPlayerById(id); + if (victim == null || !victim.IsAlive()) continue; + victim.Suicide(PlayerState.DeathReason.Kamikazed, kamikazePc); + } + + kvp.Value.Clear(); + MarkedPlayers.Remove(kvp.Key); + Logger.Info($"Murder {kamikazePc.GetRealName()}'s targets: {string.Join(", ", kvp.Value.Select(x => GetPlayerById(x).GetNameWithRole()))}", "Kamikaze"); + } + } + } +} diff --git a/Roles/Impostor/Kidnapper.cs b/Roles/Impostor/Kidnapper.cs new file mode 100644 index 000000000..5031567fa --- /dev/null +++ b/Roles/Impostor/Kidnapper.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static TOHE.Options; +using static TOHE.Translator; +using static TOHE.Utils; + +namespace TOHE.Roles.Impostor +{ + internal class Kidnapper + { + private static int Id => 643300; + public static OptionItem SSCD; + public static void SetupCustomOption() + { + SetupRoleOptions(Id, TabGroup.ImpostorRoles, CustomRoles.Kidnapper); + SSCD = FloatOptionItem.Create(Id + 2, "ShapeshiftCooldown", new(0f, 180f, 2.5f), 30f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Kidnapper]) + .SetValueFormat(OptionFormat.Seconds); + } + public static void OnShapeshift(PlayerControl kidnapper, PlayerControl target) + { + if (kidnapper == null || target == null) return; + target.TP(kidnapper); + } + } +} diff --git a/main.cs b/main.cs index 0b7c4293f..fa0dd8221 100644 --- a/main.cs +++ b/main.cs @@ -678,6 +678,8 @@ public enum CustomRoles Hangman, Hitman, Inhibitor, + Kamikaze, + Kidnapper, Minimalism, // Killing Machine BallLightning, // Lightning Librarian,