diff --git a/GameModes/CopsAndRobbersManager.cs b/GameModes/CopsAndRobbersManager.cs new file mode 100644 index 000000000..2b6991f55 --- /dev/null +++ b/GameModes/CopsAndRobbersManager.cs @@ -0,0 +1,1393 @@ +using AmongUs.GameOptions; +using static TOHE.Translator; +using UnityEngine; +using Hazel; +using TOHE.Modules; +using AmongUs.Data; +using System.Text; +using System; + +namespace TOHE; + +internal static class CopsAndRobbersManager +{ + private const int Id = 67_224_001; + + public static readonly HashSet cops = []; + public static readonly HashSet robbers = []; + public static readonly Dictionary captured = []; + + private static readonly Dictionary capturedScore = []; + private static readonly Dictionary capturedTime = []; + private static readonly Dictionary releaseTime = []; + private static readonly Dictionary timesCaptured = []; + private static readonly Dictionary saved = []; + private static readonly Dictionary defaultSpeed = []; + + private static readonly Dictionary copAbilityChances = []; + private static readonly Dictionary robberAbilityChances = []; + private static readonly Dictionary RemoveCopAbility = []; + private static readonly Dictionary trapLocation = []; + private static readonly HashSet removeTrap = []; + private static readonly Dictionary spikeTrigger = []; + private static readonly Dictionary flashTrigger = []; + private static readonly Dictionary k9 = []; + private static readonly Dictionary scopeAltered = []; + private static int killDistance; + public static Dictionary> roleSettings = []; + + private static readonly Dictionary RemoveRobberAbility = []; + private static readonly HashSet energyShieldActive = []; + private static readonly HashSet smokeBombActive = []; + private static readonly Dictionary smokeBombTriggered = []; + private static readonly HashSet adrenalineRushActive = []; + private static readonly Dictionary radar = []; + private static readonly HashSet disguise = []; + + private static int numCops; + private static int numCaptures; + private static int numRobbers; + public static int RoundTime; + + public static OptionItem CandR_NumCops; + private static OptionItem CandR_CaptureCooldown; + private static OptionItem CandR_TeleportCaptureToRandomLoc; + private static OptionItem CandR_CopAbilityTriggerChance; + private static OptionItem CandR_CopAbilityCooldown; + private static OptionItem CandR_CopAbilityDuration; + private static OptionItem CandR_HotPursuitChance; + private static OptionItem CandR_HotPursuitSpeed; + private static OptionItem CandR_SpikeStripChance; + private static OptionItem CandR_SpikeStripRadius; + private static OptionItem CandR_SpikeStripDuration; + private static OptionItem CandR_FlashBangChance; + private static OptionItem CandR_FlashBangRadius; + private static OptionItem CandR_FlashBangDuration; + private static OptionItem CandR_K9Chance; + private static OptionItem CandR_ScopeChance; + private static OptionItem CandR_ScopeIncrease; + + + + private static OptionItem CandR_NotifyRobbersWhenCaptured; + private static OptionItem CandR_RobberVentDuration; + private static OptionItem CandR_RobberVentCooldown; + private static OptionItem CandR_RobberAbilityDuration; + private static OptionItem CandR_RobberAbilityTriggerChance; + private static OptionItem CandR_AdrenalineRushChance; + private static OptionItem CandR_AdrenalineRushSpeed; + private static OptionItem CandR_EnergyShieldChance; + private static OptionItem CandR_SmokeBombChance; + private static OptionItem CandR_SmokeBombDuration; + private static OptionItem CandR_DisguiseChance; + private static OptionItem CandR_RadarChance; + private static OptionItem CandR_ReleaseCooldownForCaptured; + private static OptionItem CandR_ReleaseCooldownForRobber; + private static OptionItem CandR_GameTime; + public static OptionItem CandR_ShowChatInGame; + + + public static void SetupCustomOption() + { + CandR_GameTime = IntegerOptionItem.Create(Id, "CandR_GameTime", new(30, 600, 10), 300, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetValueFormat(OptionFormat.Seconds) + .SetHeader(true); + + CandR_ShowChatInGame = CandR_NotifyRobbersWhenCaptured = BooleanOptionItem.Create(Id + 1, "C&R_ShowChatInGame", false, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR); + + /*********** Cops ***********/ + TextOptionItem.Create(Id + 2, "Cop", TabGroup.ModSettings) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)); + + CandR_NumCops = IntegerOptionItem.Create(Id + 3, "C&R_NumCops", new(1, 5, 1), 2, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)); + CandR_CaptureCooldown = FloatOptionItem.Create(Id + 4, "C&R_CaptureCooldown", new(5f, 60f, 2.5f), 15f, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds); + CandR_TeleportCaptureToRandomLoc = BooleanOptionItem.Create(Id + 5, "C&R_TeleportCaptureToRandomLoc", true, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)); + + CandR_CopAbilityTriggerChance = IntegerOptionItem.Create(Id + 6, "C&R_CopAbilityTriggerChance", new(0, 100, 5), 50, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent); + + CandR_CopAbilityDuration = IntegerOptionItem.Create(Id + 7, "C&R_CopAbilityDuration", new(1, 10, 1), 10, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds) + .SetParent(CandR_CopAbilityTriggerChance); + CandR_CopAbilityCooldown = FloatOptionItem.Create(Id + 8, "C&R_CopAbilityCooldown", new(10f, 60f, 2.5f), 20f, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds) + .SetParent(CandR_CopAbilityTriggerChance); + + + CandR_HotPursuitChance = IntegerOptionItem.Create(Id + 9, "C&R_HotPursuitChance", new(0, 100, 5), 35, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_CopAbilityTriggerChance); + CandR_HotPursuitSpeed = FloatOptionItem.Create(Id + 10, "C&R_HotPursuitSpeed", new(0f, 2f, 0.25f), 1f, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Multiplier) + .SetParent(CandR_HotPursuitChance); + + + CandR_SpikeStripChance = IntegerOptionItem.Create(Id + 11, "C&R_SpikeStripChance", new(0, 100, 5), 20, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_CopAbilityTriggerChance); + CandR_SpikeStripRadius = FloatOptionItem.Create(Id + 12, "C&R_SpikeStripRadius", new(0.5f, 2f, 0.5f), 1f, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Multiplier) + .SetParent(CandR_SpikeStripChance); + CandR_SpikeStripDuration = IntegerOptionItem.Create(Id + 13, "C&R_SpikeStripDuration", new(1, 10, 1), 5, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds) + .SetParent(CandR_SpikeStripChance); + + CandR_FlashBangChance = IntegerOptionItem.Create(Id + 14, "C&R_FlashBangChance", new(0, 100, 5), 15, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_CopAbilityTriggerChance); + CandR_FlashBangRadius = FloatOptionItem.Create(Id + 15, "C&R_FlashBangRadius", new(0.5f, 2f, 0.5f), 1f, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Multiplier) + .SetParent(CandR_FlashBangChance); + CandR_FlashBangDuration = IntegerOptionItem.Create(Id + 16, "C&R_FlashBangDuration", new(1, 10, 1), 5, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds) + .SetParent(CandR_FlashBangChance); + + CandR_ScopeChance = IntegerOptionItem.Create(Id + 17, "C&R_ScopeChance", new(0, 100, 5), 10, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_CopAbilityTriggerChance); + CandR_ScopeIncrease = IntegerOptionItem.Create(Id + 18, "C&R_ScopeIncrease", new(1, 5, 1), 1, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Multiplier) + .SetParent(CandR_ScopeChance); + + CandR_K9Chance = IntegerOptionItem.Create(Id + 19, "C&R_K9Chance", new(0, 100, 5), 20, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(0, 123, 255, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_CopAbilityTriggerChance); + + roleSettings[CustomRoles.Cop] = [CandR_NumCops, CandR_CaptureCooldown, CandR_CopAbilityTriggerChance]; + + /*********** Robbers ***********/ + + TextOptionItem.Create(Id + 20, "Robber", TabGroup.ModSettings) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)); + + CandR_NotifyRobbersWhenCaptured = BooleanOptionItem.Create(Id + 21, "C&R_NotifyRobbersWhenCaptured", true, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)); + + CandR_RobberVentDuration = FloatOptionItem.Create(Id + 22, "C&R_RobberVentDuration", new(1f, 20f, 0.5f), 10f, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds); + CandR_RobberVentCooldown = FloatOptionItem.Create(Id + 23, "C&R_RobberVentCooldown", new(10f, 60f, 2.5f), 20f, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds); + CandR_ReleaseCooldownForCaptured = IntegerOptionItem.Create(Id + 24, "C&R_ReleaseCooldownForCaptured", new(5, 20, 1), 5, TabGroup.ModSettings, false) + .SetGameMode (CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds); + CandR_ReleaseCooldownForRobber = IntegerOptionItem.Create(Id + 25, "C&R_ReleaseCooldownForRobber", new(5, 20, 1), 15, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds); + CandR_RobberAbilityTriggerChance = IntegerOptionItem.Create(Id + 26, "C&R_RobberAbilityTriggerChance", new(0, 100, 5), 50, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent); + CandR_RobberAbilityDuration = IntegerOptionItem.Create(Id + 27, "C&R_RobberAbilityDuration", new(1, 10, 1), 10, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds) + .SetParent(CandR_RobberAbilityTriggerChance); + + CandR_AdrenalineRushChance = IntegerOptionItem.Create(Id + 28, "C&R_AdrenalineRushChance", new(0, 100, 5), 30, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_RobberAbilityTriggerChance); + CandR_AdrenalineRushSpeed = FloatOptionItem.Create(Id + 29, "C&R_AdrenalineRushSpeed", new(0f, 2f, 0.25f), 1f, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Multiplier) + .SetParent(CandR_AdrenalineRushChance); + + CandR_EnergyShieldChance = IntegerOptionItem.Create(Id + 30, "C&R_EnergyShieldChance", new(0, 100, 5), 25, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_RobberAbilityTriggerChance); + + CandR_SmokeBombChance = IntegerOptionItem.Create(Id + 31, "C&R_SmokeBombChance", new(0, 100, 5), 20, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_RobberAbilityTriggerChance); + CandR_SmokeBombDuration = IntegerOptionItem.Create(Id + 32, "C&R_SmokeBombDuration", new(1, 10, 1), 5, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds) + .SetParent(CandR_SmokeBombChance); + + CandR_DisguiseChance = IntegerOptionItem.Create(Id + 33, "C&R_DisguiseChance", new(0, 100, 5), 10, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_RobberAbilityTriggerChance); + + CandR_RadarChance = IntegerOptionItem.Create(Id + 34, "C&R_RadarChance", new(0, 100, 5), 15, TabGroup.ModSettings, false) + .SetGameMode(CustomGameMode.CandR) + .SetColor(new Color32(255, 140, 0, byte.MaxValue)) + .SetValueFormat(OptionFormat.Percent) + .SetParent(CandR_RobberAbilityTriggerChance); + + roleSettings[CustomRoles.Robber] = [CandR_NotifyRobbersWhenCaptured, CandR_RobberVentDuration, CandR_RobberVentCooldown, CandR_ReleaseCooldownForCaptured, CandR_ReleaseCooldownForRobber, CandR_RobberAbilityTriggerChance]; + } + + public enum RoleType + { + Cop, + Robber, + Captured + } + private enum RobberAbility + { + AdrenalineRush, // Increases speed for a set amount of time. + EnergyShield, // Protects from the next capture attempt. + SmokeBomb, // Blinds the Cop when captured. + Disguise, // Wear the same costume as a Cop for confusion. + Radar // Points towards closest captured player, if no captured player, it points towards a cop (colored arrows indicating cops or captured) + } + private enum CopAbility + { + HotPursuit, // Speed boost for a set amount of time. + SpikeStrip, // Sets a trap that slows down the robber. + FlashBang, // Sets a trap that blinds the robber temporarily. + K9, // Points to the closest robber. + Scope // Increases the capture range. + } + + public static bool HasTasks(CustomRoles role) + { + return role switch + { + CustomRoles.Cop => false, + CustomRoles.Robber => true, + _ => false, + }; + } + + private static void CumulativeAbilityChances() + { + Dictionary copOptionItems = new() + { + {CopAbility.HotPursuit, CandR_HotPursuitChance }, + {CopAbility.SpikeStrip, CandR_SpikeStripChance }, + {CopAbility.FlashBang, CandR_FlashBangChance }, + {CopAbility.K9, CandR_K9Chance }, + {CopAbility.Scope, CandR_ScopeChance }, + }; + copAbilityChances.Clear(); + var cumulativeChance = 0; + foreach (var optionItem in copOptionItems) + { + if (optionItem.Value.GetInt() == 0) continue; + cumulativeChance += optionItem.Value.GetInt(); + copAbilityChances.Add(optionItem.Key, cumulativeChance); + } + + Dictionary robberOptionItems = new() + { + {RobberAbility.AdrenalineRush, CandR_AdrenalineRushChance }, + {RobberAbility.EnergyShield, CandR_EnergyShieldChance }, + {RobberAbility.SmokeBomb, CandR_SmokeBombChance }, + {RobberAbility.Radar, CandR_RadarChance }, + {RobberAbility.Disguise, CandR_DisguiseChance }, + }; + robberAbilityChances.Clear(); + cumulativeChance = 0; + foreach (var optionItem in robberOptionItems) + { + if (optionItem.Value.GetInt() == 0) continue; + cumulativeChance += optionItem.Value.GetInt(); + robberAbilityChances.Add(optionItem.Key, cumulativeChance); + } + } + + public static void Init() + { + if (Options.CurrentGameMode != CustomGameMode.CandR) return; + + cops.Clear(); + robbers.Clear(); + captured.Clear(); + numCaptures = 0; + numRobbers = 0; + capturedScore.Clear(); + timesCaptured.Clear(); + saved.Clear(); + numCops = CandR_NumCops.GetInt(); + defaultSpeed.Clear(); + RemoveCopAbility.Clear(); + RemoveRobberAbility.Clear(); + trapLocation.Clear(); + removeTrap.Clear(); + spikeTrigger.Clear(); + flashTrigger.Clear(); + k9.Clear(); + scopeAltered.Clear(); + energyShieldActive.Clear(); + smokeBombActive.Clear(); + smokeBombTriggered.Clear(); + adrenalineRushActive.Clear(); + radar.Clear(); + disguise.Clear(); + killDistance = Main.RealOptionsData.GetInt(Int32OptionNames.KillDistance); + CumulativeAbilityChances(); + capturedTime.Clear(); + releaseTime.Clear(); + } + public static void SetData() + { + if (Options.CurrentGameMode != CustomGameMode.CandR) return; + + RoundTime = CandR_GameTime.GetInt() + 8; + var now = Utils.GetTimeStamp() + 8; + foreach (byte robber in robbers) + { + releaseTime[robber] = now; + } + } + public static Dictionary SetRoles() + { + Dictionary finalRoles = []; + var random = IRandom.Instance; + List AllPlayers = Main.AllPlayerControls.Shuffle(random).ToList(); + + if (Main.EnableGM.Value) + { + finalRoles[PlayerControl.LocalPlayer.PlayerId] = CustomRoles.GM; + AllPlayers.Remove(PlayerControl.LocalPlayer); + } + + int optImpNum = numCops; + foreach (PlayerControl pc in AllPlayers) + { + if (pc == null) continue; + if (optImpNum > 0) + { + finalRoles[pc.PlayerId] = CustomRoles.Cop; + RoleType.Cop.Add(pc.PlayerId); + optImpNum--; + } + else + { + finalRoles[pc.PlayerId] = CustomRoles.Robber; + RoleType.Robber.Add(pc.PlayerId); + } + Logger.Msg($"set role for {pc.PlayerId}: {finalRoles[pc.PlayerId]}", "SetRoles"); + } + SendCandRData(5); + SendCandRData(6); + return finalRoles; + } + private static void Add(this RoleType role, byte playerId) + { + defaultSpeed[playerId] = Main.AllPlayerSpeed[playerId]; + role.SetCostume(playerId: playerId); + + switch (role) + { + case RoleType.Cop: + cops.Add(playerId); + capturedScore[playerId] = 0; + Main.UnShapeShifter.Add(playerId); + return; + + case RoleType.Robber: + robbers.Add(playerId); + timesCaptured[playerId] = 0; + saved[playerId] = 0; + numRobbers++; + return; + } + } + private static void AddCaptured(this PlayerControl robber, Vector2 capturedLocation) + { + RoleType.Captured.SetCostume(playerId: robber.PlayerId); + if (CandR_TeleportCaptureToRandomLoc.GetBool()) + { + var rand = IRandom.Instance; + if (rand.Next(100) > 50) + { + RandomSpawn.SpawnMap spawnMap = Utils.GetActiveMapName() switch + { + MapNames.Skeld => new RandomSpawn.SkeldSpawnMap(), + MapNames.Mira => new RandomSpawn.MiraHQSpawnMap(), + MapNames.Polus => new RandomSpawn.PolusSpawnMap(), + MapNames.Dleks => new RandomSpawn.DleksSpawnMap(), + MapNames.Fungle => new RandomSpawn.FungleSpawnMap(), + MapNames.Airship => new RandomSpawn.AirshipSpawnMap(), + _ => null, + }; + if (spawnMap != null) + { + capturedLocation = spawnMap.GetLocation(); + + } + } + else + { + var vent = ShipStatus.Instance.AllVents.RandomElement(); + capturedLocation = new Vector2(vent.transform.position.x, vent.transform.position.y + 0.3636f); + } + robber.RpcTeleport(capturedLocation); + + } + captured[robber.PlayerId] = capturedLocation; + Main.AllPlayerSpeed[robber.PlayerId] = Main.MinSpeed; + robber.RpcSetVentInteraction(); + robber?.MarkDirtySettings(); + } + private static void RemoveCaptured(this PlayerControl rescued) + { + if (rescued == null) return; + captured.Remove(rescued.PlayerId); + if (disguise.Contains(rescued.PlayerId)) RoleType.Cop.SetCostume(playerId: rescued.PlayerId); + else RoleType.Robber.SetCostume(playerId: rescued.PlayerId); //for robber + Main.AllPlayerSpeed[rescued.PlayerId] = defaultSpeed[rescued.PlayerId]; + if (adrenalineRushActive.Contains(rescued.PlayerId)) Main.AllPlayerSpeed[rescued.PlayerId] += CandR_AdrenalineRushSpeed.GetFloat(); + rescued.RpcSetVentInteraction(); + rescued?.MarkDirtySettings(); + } + + public static void SetCostume(this RoleType opMode, byte playerId) + { + if (playerId == byte.MaxValue) return; + PlayerControl player = Utils.GetPlayerById(playerId); + if (player == null) return; + var playerOutfit = new NetworkedPlayerInfo.PlayerOutfit(); + switch (opMode) + { + case RoleType.Cop: + + playerOutfit.Set(player.GetRealName(isMeeting:true), + 1, //blue + "hat_police", //hat + "skin_Police", //skin + "visor_pk01_Security1Visor", //visor + player.CurrentOutfit.PetId, + player.CurrentOutfit.NamePlateId); + break; + case RoleType.Robber: + playerOutfit.Set(player.GetRealName(isMeeting: true), + 6, //black + "hat_pk04_Vagabond", //hat + "skin_None", //skin + "visor_None", //visor + player.CurrentOutfit.PetId, + player.CurrentOutfit.NamePlateId); + break; + case RoleType.Captured: + playerOutfit.Set(player.GetRealName(isMeeting: true), + 5, //yellow + "hat_tombstone", //hat + "skin_prisoner", //skin + "visor_pk01_DumStickerVisor", //visor + player.CurrentOutfit.PetId, + player.CurrentOutfit.NamePlateId); + break; + } + player.SetNewOutfit(newOutfit: playerOutfit, setName: false, setNamePlate: false); + Main.OvverideOutfit[player.PlayerId] = (playerOutfit, Main.PlayerStates[player.PlayerId].NormalOutfit.PlayerName); + } + public static void CaptureCooldown(PlayerControl cop) => + Main.AllPlayerKillCooldown[cop.PlayerId] = CandR_CaptureCooldown.GetFloat(); + + private static void SendCandRData(byte op, byte playerId = byte.MaxValue, int capturedCount = 0) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncCandRData, SendOption.Reliable, -1); + writer.Write(op); + switch (op) + { + case 0: + writer.Write(playerId); + writer.Write(k9[playerId]); + break; + case 1: + writer.Write(playerId); + break; + case 2: + writer.Write(playerId); + writer.Write(radar[playerId]); + break; + case 3: + writer.Write(playerId); + break; + case 4: + writer.Write(capturedCount); + break; + case 5: + writer.Write(numRobbers); + break; + case 6: + writer.Write(cops.Count); + foreach (var pid in cops) writer.Write(pid); + writer.Write(robbers.Count); + foreach (var pid in robbers) writer.Write(pid); + break; + case 7: + writer.Write(playerId); + writer.Write(saved[playerId]); + break; + case 8: + writer.Write(playerId); + writer.Write(capturedScore[playerId]); + break; + } + + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ReceiveCandRData(MessageReader reader) + { + byte op = reader.ReadByte(); + switch (op) + { + case 0: + byte copId = reader.ReadByte(); + byte k9Id = reader.ReadByte(); + k9[copId] = k9Id; + break; + case 1: + byte removeCopId = reader.ReadByte(); + k9.Remove(removeCopId); + break; + case 2: + byte robberId = reader.ReadByte(); + byte radarId = reader.ReadByte(); + radar[robberId] = radarId; + break; + case 3: + byte removeRobberId = reader.ReadByte(); + radar.Remove(removeRobberId); + break; + case 4: + numCaptures = reader.ReadInt32(); + break; + case 5: + numRobbers = reader.ReadInt32(); + break; + case 6: + cops.Clear(); + int copLength = reader.ReadInt32(); + for (int i = 0; i < copLength; i++) cops.Add(reader.ReadByte()); + robbers.Clear(); + int robberLength = reader.ReadInt32(); + for (int i = 0; i < robberLength; i++) robbers.Add(reader.ReadByte()); + break; + case 7: + byte robber = reader.ReadByte(); + saved[robber] = reader.ReadInt32(); + break; + case 8: + byte cop = reader.ReadByte(); + capturedScore[cop] = reader.ReadInt32(); + break; + } + } + + private static CopAbility? RandomCopAbility() + { + var random = IRandom.Instance; + var shouldTrigger = random.Next(100); + if (copAbilityChances.Count == 0 || shouldTrigger >= CandR_CopAbilityTriggerChance.GetInt()) return null; + + int randomChance = random.Next(copAbilityChances.Values.Last()); + + foreach (var ability in copAbilityChances) + { + if (randomChance < ability.Value) + { + return ability.Key; + } + } + return null; // shouldn't happen + } + private static void DeactivateCopAbility(this PlayerControl cop, CopAbility? ability, Vector2 loc) + { + if (ability == null || cop == null) return; + switch (ability) + { + case CopAbility.HotPursuit: + Main.AllPlayerSpeed[cop.PlayerId] -= CandR_HotPursuitSpeed.GetFloat(); + cop.MarkDirtySettings(); + break; + case CopAbility.SpikeStrip: //it might also be removed in fixed update when trap triggered. + case CopAbility.FlashBang: //it might also be removed in fixed update when trap triggered. + trapLocation.Remove(loc); + break; + case CopAbility.K9: + byte targetId = k9[cop.PlayerId]; + Logger.Info($"Removed k9 for {cop.PlayerId}", "Remove k9"); + k9.Remove(cop.PlayerId); + SendCandRData(1, cop.PlayerId); + TargetArrow.Remove(cop.PlayerId, targetId); + break; + case CopAbility.Scope: + scopeAltered.Remove(cop.PlayerId); + cop.MarkDirtySettings(); + break; + default: + return; + } + RemoveCopAbility.Remove(cop.PlayerId); + } + private static void ActivateCopAbility(this PlayerControl cop, CopAbility? ability) + { + Vector2 loc = cop.GetCustomPosition(); + switch (ability) + { + case CopAbility.HotPursuit: //increase speed of cop + Main.AllPlayerSpeed[cop.PlayerId] += CandR_HotPursuitSpeed.GetFloat(); + cop?.MarkDirtySettings(); + break; + case CopAbility.SpikeStrip: //add location of spike trap, when triggered, player speed will reduce + case CopAbility.FlashBang: //add location of flash trap, when triggered, player vision will reduce + if (trapLocation.ContainsKey(loc)) + { + Logger.Info("Location was already trapped", "SpikeStrip activate"); + return; + } + trapLocation.Add(loc, ability); + break; + + case CopAbility.K9: + if (k9.ContainsKey(cop.PlayerId)) + return; + k9.Add(cop.PlayerId, byte.MaxValue); + SendCandRData(0, cop.PlayerId); + Logger.Info($"Added {cop.PlayerId} for k9", "Ability activated"); + break; + case CopAbility.Scope: + if (scopeAltered.ContainsKey(cop.PlayerId)) + return; + scopeAltered[cop.PlayerId] = false; + cop.MarkDirtySettings(); + break; + default: + return; + } + RemoveCopAbility[cop.PlayerId] = ability; + var notifyMsg = GetString("C&R_AbilityActivated"); + cop.Notify(string.Format(notifyMsg.Replace("{Ability.Name}", "{0}"), GetString($"CopAbility.{ability}")), CandR_CopAbilityDuration.GetFloat()); + _ = new LateTask(() => + { + if (!GameStates.IsInGame || !RemoveCopAbility.ContainsKey(cop.PlayerId)) return; + cop.DeactivateCopAbility(ability: ability, loc: loc); + }, CandR_CopAbilityDuration.GetInt(), "Remove cop ability"); + } + public static void UnShapeShiftButton(PlayerControl shapeshifter) + { + if (!AmongUsClient.Instance.AmHost) return; + if (shapeshifter == null) return; + if (!shapeshifter.Is(CustomRoles.Cop)) return; + + + CopAbility? ability = RandomCopAbility(); + if (ability == null) return; + shapeshifter.ActivateCopAbility(ability); + Logger.Info($"Activating {ability} for id: {shapeshifter.PlayerId}", "C&R OnCheckShapeshift"); + return; + } + public static string GetClosestArrow(PlayerControl seer, PlayerControl target, bool isForMeeting = false) + { + if (isForMeeting || seer.PlayerId != target.PlayerId) return string.Empty; + if (k9.ContainsKey(seer.PlayerId)) + return Utils.ColorString(Utils.GetRoleColor(CustomRoles.Cop), TargetArrow.GetArrows(seer)); + else if (radar.ContainsKey(seer.PlayerId)) + { + bool isCaptured = captured.ContainsKey(radar[seer.PlayerId]); + return Utils.ColorString(isCaptured? Utils.GetRoleColor(CustomRoles.Robber) : Utils.GetRoleColor(CustomRoles.Cop), TargetArrow.GetArrows(seer)); + } + return string.Empty; + } + + public static void OnCopAttack(PlayerControl cop, PlayerControl robber) + { + if (cop == null || robber == null || Options.CurrentGameMode != CustomGameMode.CandR) return; + if (!cop.Is(CustomRoles.Cop) || !robber.Is(CustomRoles.Robber)) return; + + if (captured.ContainsKey(robber.PlayerId)) + { + cop.Notify("C&R_AlreadyCaptured"); + return; + } + if (robber.inVent) + { + Logger.Info($"Robber, playerID {robber.PlayerId}, is in a vent, capture blocked", "C&R"); + return; + } + + if (!energyShieldActive.Contains(robber.PlayerId)) + { + Vector2 robberLocation = robber.GetCustomPosition(); + robber.AddCaptured(robberLocation); + numCaptures = captured.Count; + foreach (var pid in cops) + { + var player = Utils.GetPlayerById(pid); + if (player != null) + { + Utils.NotifyRoles(SpecifySeer: player); + } + } + + if (CandR_NotifyRobbersWhenCaptured.GetBool()) + { + foreach (byte pid in robbers) + { + if (pid == byte.MaxValue) continue; + PlayerControl pc = Utils.GetPlayerById(pid); + pc?.KillFlash(); + } + } + + if (!capturedScore.ContainsKey(cop.PlayerId)) capturedScore[cop.PlayerId] = 0; + capturedScore[cop.PlayerId]++; + + capturedTime[robber.PlayerId] = Utils.GetTimeStamp(); + + if (!timesCaptured.ContainsKey(robber.PlayerId)) timesCaptured[robber.PlayerId] = 0; + timesCaptured[robber.PlayerId]++; + SendCandRData(4, capturedCount: numCaptures); + SendCandRData(8, playerId: cop.PlayerId); + } + else + { + Logger.Info($"capture canceled, Energy shield active {robber.PlayerId}", "Capture canceled"); + cop.Notify($"Could not capture, {GetString("RobberAbility.EnergyShield")} active"); + } + if (smokeBombActive.Contains(robber.PlayerId)) + { + smokeBombTriggered[cop.PlayerId] = Utils.GetTimeStamp(); + cop.MarkDirtySettings(); + smokeBombActive.Remove(robber.PlayerId); + Logger.Info($"smoke bomb triggered for {cop.PlayerId}", "smoke trigger"); + } + CaptureCooldown(cop); + cop.ResetKillCooldown(); + cop.SetKillCooldown(); + } + private static RobberAbility? RandomRobberAbility() + { + var random = IRandom.Instance; + var shouldTrigger = random.Next(100); + if (!robberAbilityChances.Any() || shouldTrigger >= CandR_RobberAbilityTriggerChance.GetInt()) return null; + + int randomChance = random.Next(copAbilityChances.Values.Last()); + foreach (var ability in robberAbilityChances) + { + if (randomChance < ability.Value) + { + return ability.Key; + } + } + return null; // shouldn't happen + } + private static void DeactivateRobberAbility(this PlayerControl robber, RobberAbility? ability) + { + if (ability == null || robber == null) return; + switch (ability) + { + case RobberAbility.AdrenalineRush: + adrenalineRushActive.Remove(robber.PlayerId); + if (!spikeTrigger.ContainsKey(robber.PlayerId)) + { + Main.AllPlayerSpeed[robber.PlayerId] -= CandR_AdrenalineRushSpeed.GetFloat(); + robber.MarkDirtySettings(); + } + break; + case RobberAbility.EnergyShield: + energyShieldActive.Remove(robber.PlayerId); + break; + case RobberAbility.SmokeBomb: + smokeBombActive.Remove(robber.PlayerId); + break; + case RobberAbility.Disguise: + disguise.Remove(robber.PlayerId); + if (captured.ContainsKey(robber.PlayerId)) + { + Logger.Info($"disguise finished for captured player {robber.PlayerId}", "disguise finish"); + break; + } + RoleType.Robber.SetCostume(robber.PlayerId); + Logger.Info($"reverting costume because disguise finished for player: {robber.PlayerId}", "disguise finish"); + break; + case RobberAbility.Radar: + byte targetId = radar[robber.PlayerId]; + Logger.Info($"Removed k9 for {robber.PlayerId}", "Remove k9"); + radar.Remove(robber.PlayerId); + SendCandRData(3, robber.PlayerId); + TargetArrow.Remove(robber.PlayerId, targetId); + break; + default: + return; + } + RemoveRobberAbility.Remove(robber.PlayerId); + } + private static void ActivateRobberAbility(this PlayerControl robber, RobberAbility? ability) + { + if (ability == null || robber == null) return; + switch (ability) + { + case RobberAbility.AdrenalineRush: + adrenalineRushActive.Add(robber.PlayerId); + Main.AllPlayerSpeed[robber.PlayerId] += CandR_AdrenalineRushSpeed.GetFloat(); + robber.MarkDirtySettings(); + break; + case RobberAbility.EnergyShield: + energyShieldActive.Add(robber.PlayerId); + break; + case RobberAbility.SmokeBomb: + smokeBombActive.Add(robber.PlayerId); + break; + case RobberAbility.Disguise: + RoleType.Cop.SetCostume(robber.PlayerId); + disguise.Add(robber.PlayerId); + break; + case RobberAbility.Radar: + if (radar.ContainsKey(robber.PlayerId)) + return; + radar.Add(robber.PlayerId, byte.MaxValue); + SendCandRData(2, robber.PlayerId); + Logger.Info($"Added {robber.PlayerId} for radar", "radar activated"); + break; + default: + return; + } + RemoveRobberAbility[robber.PlayerId] = ability; + var notifyMsg = GetString("C&R_AbilityActivated"); + robber.Notify(string.Format(notifyMsg.Replace("{Ability.Name}", "{0}"), GetString($"RobberAbility.{ability}")), CandR_RobberAbilityDuration.GetFloat()); + _ = new LateTask(() => + { + if (!GameStates.IsInGame || !RemoveRobberAbility.ContainsKey(robber.PlayerId)) return; + robber.DeactivateRobberAbility(ability: ability); + }, CandR_RobberAbilityDuration.GetInt(), "Remove robber ability"); + } + public static void OnRobberExitVent(PlayerControl pc) + { + if (pc == null) return; + if (!pc.Is(CustomRoles.Robber)) return; + if (captured.ContainsKey(pc.PlayerId)) + { + Logger.Info($"Player {pc.PlayerId} was captured", "robber activity cancel"); + return; + } + if (spikeTrigger.ContainsKey(pc.PlayerId)) + { + Logger.Info($"Ability canceled for {pc.PlayerId}, robber triggered spike strip", "robber ability cancel"); + return; + } + var ability = RandomRobberAbility(); + if (ability == null) return; + + float delay = Utils.GetActiveMapId() != 5 ? 0.1f : 0.4f; + _ = new LateTask(() => + { + ActivateRobberAbility(pc, ability); + }, delay, "Robber On Exit Vent"); + } + + public static void ApplyGameOptions(ref IGameOptions opt, PlayerControl player) + { + if (player.Is(CustomRoles.Cop) && CandR_CopAbilityTriggerChance.GetFloat() > 0f) + { + AURoleOptions.ShapeshifterCooldown = CandR_CopAbilityCooldown.GetFloat(); + AURoleOptions.ShapeshifterDuration = CandR_CopAbilityDuration.GetFloat(); + } + + if (player.Is(CustomRoles.Robber)) + { + AURoleOptions.EngineerCooldown = CandR_RobberVentCooldown.GetFloat(); + AURoleOptions.EngineerInVentMaxTime = CandR_RobberVentDuration.GetFloat(); + } + + if (scopeAltered.TryGetValue(player.PlayerId, out bool isAltered)) + { + if (!isAltered) + { + killDistance = opt.GetInt(Int32OptionNames.KillDistance) + CandR_ScopeIncrease.GetInt(); + scopeAltered[player.PlayerId] = true; + } + opt.SetInt(Int32OptionNames.KillDistance, killDistance); + } + if (flashTrigger.ContainsKey(player.PlayerId) || smokeBombTriggered.ContainsKey(player.PlayerId)) + { + opt.SetVision(false); + opt.SetFloat(FloatOptionNames.CrewLightMod, 0.05f); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, 0.05f); + Logger.Warn($"vision for {player.PlayerId} set to 0.05f", "blind vision"); + } + else + { + opt.SetVision(player.Is(CustomRoles.Cop)); + opt.SetFloat(FloatOptionNames.CrewLightMod, Main.DefaultCrewmateVision); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Main.DefaultImpostorVision); + } + return; + } + + public static void AbilityDescription(string ability, byte playerId = byte.MaxValue) + { + ability = ability.ToLower().Trim().TrimStart('*').Replace(" ", string.Empty); + StringBuilder copAbilities = new(); + int i = 0; + foreach (var ab in Enum.GetValues(typeof(CopAbility))) + { + i++; + copAbilities.Append($"{i}. {GetString($"CopAbility.{ab}")}:\n"); + copAbilities.Append($"{GetString($"Description.{ab}")}\n\n"); + } + StringBuilder robberAbilities = new(); + i = 0; + foreach (var ab in Enum.GetValues(typeof(RobberAbility))) + { + i++; + robberAbilities.Append($"{i}. {GetString($"RobberAbility.{ab}")}:\n"); + robberAbilities.Append($"{GetString($"Description.{ab}")}\n\n"); + } + + if (ability == GetString(CustomRoles.Cop.ToString()).ToLower().Trim().TrimStart('*').Replace(" ", string.Empty) || ability == "cop") + { + Utils.SendMessage(copAbilities.ToString(), sendTo:playerId, title: Utils.ColorString(Utils.GetRoleColor(CustomRoles.Cop), Utils.GetRoleName(CustomRoles.Cop))); + } + else if (ability == GetString(CustomRoles.Cop.ToString()).ToLower().Trim().TrimStart('*').Replace(" ", string.Empty) || ability == "robber") + { + Utils.SendMessage(robberAbilities.ToString(), sendTo: playerId, title: Utils.ColorString(Utils.GetRoleColor(CustomRoles.Robber), Utils.GetRoleName(CustomRoles.Robber))); + } + else + { + Utils.SendMessage(copAbilities.ToString(), sendTo: playerId, title: Utils.ColorString(Utils.GetRoleColor(CustomRoles.Cop), Utils.GetRoleName(CustomRoles.Cop))); + Utils.SendMessage(robberAbilities.ToString(), sendTo: playerId, title: Utils.ColorString(Utils.GetRoleColor(CustomRoles.Robber), Utils.GetRoleName(CustomRoles.Robber))); + } + } + + public static void SetAbilityButtonText(HudManager hud, byte playerId) + { + if (playerId == byte.MaxValue) return; + PlayerControl player = Utils.GetPlayerById(playerId); + if (player == null) return; + if (player.Is(CustomRoles.Cop)) + { + hud.AbilityButton?.OverrideText(GetString("CopAbilityText")); + hud.KillButton?.OverrideText(GetString("CopKillButtonText")); + } + } + public static string SummaryTexts(byte id, bool disableColor = true, bool check = false) + { + string name; + try + { + if (id == PlayerControl.LocalPlayer.PlayerId) name = DataManager.player.Customization.Name; + else name = Main.AllClientRealNames[GameData.Instance.GetPlayerById(id).ClientId]; + } + catch + { + Logger.Error("Failed to get name for {id} by real client names, try assign with AllPlayerNames", "Utils.SummaryTexts"); + name = Main.AllPlayerNames[id].RemoveHtmlTags().Replace("\r\n", string.Empty) ?? "ERROR"; + } + + // Impossible to output summarytexts for a player without playerState + if (!Main.PlayerStates.TryGetValue(id, out var ps)) + { + Logger.Error("playerState for {id} not found", "CopsAndRobbersManager.SummaryTexts"); + return $"[{id}]" + name + " : ERROR"; + } + + string TaskCount = string.Empty; + Color nameColor = Color.white; + string roleName = Utils.ColorString(Utils.GetRoleColor(CustomRoles.GM), Utils.GetRoleName(CustomRoles.GM)); + string capturedCountText = string.Empty; + if (robbers.Contains(id)) + { + var taskState = Main.PlayerStates?[id].TaskState; + Color CurrentСolor; + CurrentСolor = taskState.IsTaskFinished ? Color.green : Utils.GetRoleColor(CustomRoles.Robber); + TaskCount = Utils.ColorString(CurrentСolor, $" ({taskState.CompletedTasksCount}/{taskState.AllTasksCount})"); + nameColor = Utils.GetRoleColor(CustomRoles.Robber); + roleName = Utils.ColorString(nameColor, Utils.GetRoleName(CustomRoles.Robber)); + if (!saved.ContainsKey(id)) saved[id] = 0; + capturedCountText = Utils.ColorString(new Color32(255, 69, 0, byte.MaxValue), $"{GetString("Saved")}: {saved[id]}"); + } + else if (cops.Contains(id)) + { + nameColor = Utils.GetRoleColor(CustomRoles.Cop); + roleName = Utils.ColorString(nameColor, Utils.GetRoleName(CustomRoles.Cop)); + if (!capturedScore.ContainsKey(id)) capturedScore[id] = 0; + capturedCountText = Utils.ColorString(new Color32(255, 69, 0, byte.MaxValue), $"{GetString("Captured")}: {capturedScore[id]}"); + + } + + Main.PlayerStates.TryGetValue(id, out var playerState); + var disconnectedText = playerState.deathReason != PlayerState.DeathReason.etc && playerState.Disconnected ? $"({GetString("Disconnected")})" : string.Empty; + + string summary = $"{Utils.ColorString(nameColor, name)} - {roleName}{TaskCount} {capturedCountText} 『{Utils.GetVitalText(id, true)}』{disconnectedText}"; + + return check && Utils.GetDisplayRoleAndSubName(id, id, true).RemoveHtmlTags().Contains("INVALID:NotAssigned") + ? "INVALID" + : disableColor ? summary.RemoveHtmlTags() : summary; + } + + public static string GetProgressText(byte playerId) + { + string progressText = string.Empty; + if (playerId == byte.MaxValue) return progressText; + Color32 textColor = Color.white; + if (cops.Contains(playerId)) + { + progressText = $" ({numCaptures}/{numRobbers})"; + textColor = Utils.GetRoleColor(CustomRoles.Cop); + } + else if (robbers.Contains(playerId)) + { + var taskState = Main.PlayerStates?[playerId].TaskState; + string Completed = $"{taskState.CompletedTasksCount}"; + progressText= $" ({Completed}/{taskState.AllTasksCount})"; + textColor = taskState.IsTaskFinished? Color.green : Utils.GetRoleColor(CustomRoles.Robber); + } + return Utils.ColorString(textColor, progressText); + } + + public static void OnPlayerDisconnect(byte playerId) + { + if (robbers.Contains(playerId)) + { + if (captured.ContainsKey(playerId)) + { + captured.Remove(playerId); + numCaptures = captured.Count; + SendCandRData(4, capturedCount: numCaptures); + } + numRobbers--; + SendCandRData(5); + foreach (var pid in cops) + { + var player = Utils.GetPlayerById(pid); + if (player != null) + { + Utils.NotifyRoles(SpecifySeer: player); + } + } + } + } + public static string GetHudText() + { + return string.Format(GetString("FFATimeRemain"), RoundTime.ToString()); + } + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))] + class FixedUpdateInGameModeCandRPatch + { + private static long LastCheckedCop; + private static long LastCheckedRobber; + private static long lastRoundTime; + private static readonly Dictionary LastCheckedReleaseCooldownCaptured = []; + private static readonly Dictionary LastCheckedReleaseCooldownRobber = []; + public static void Postfix(PlayerControl __instance) + { + if (!GameStates.IsInTask || Options.CurrentGameMode != CustomGameMode.CandR) return; + var now = Utils.GetTimeStamp(); + + if (lastRoundTime != now) + { + lastRoundTime = now; + RoundTime--; + } + if (!AmongUsClient.Instance.AmHost) return; + + if (__instance.AmOwner) + { + if (Main.UnShapeShifter.Any(x => Utils.GetPlayerById(x) != null && Utils.GetPlayerById(x).CurrentOutfitType != PlayerOutfitType.Shapeshifted) + && !__instance.IsMushroomMixupActive() && Main.GameIsLoaded) + { + foreach (var UnShapeshifterId in Main.UnShapeShifter) + { + var UnShapeshifter = Utils.GetPlayerById(UnShapeshifterId); + if (UnShapeshifter == null) + { + Main.UnShapeShifter.Remove(UnShapeshifterId); + continue; + } + if (UnShapeshifter.CurrentOutfitType == PlayerOutfitType.Shapeshifted) continue; + + var randomPlayer = Main.AllPlayerControls.FirstOrDefault(x => x != UnShapeshifter); + UnShapeshifter.RpcShapeshift(randomPlayer, false); + UnShapeshifter.RpcRejectShapeshift(); + RoleType.Cop.SetCostume(UnShapeshifter.PlayerId); + Utils.NotifyRoles(SpecifyTarget: UnShapeshifter); + Logger.Info($"Revert to shapeshifting state for: {__instance.GetRealName()}", "UnShapeShifter_FixedUpdate"); + } + } + } + + captured.Remove(byte.MaxValue); + + robbers.Remove(byte.MaxValue); + captured.Remove(byte.MaxValue); + + Dictionary removeCaptured = []; + foreach (byte robberId in robbers) + { + if (robberId == byte.MaxValue) continue; + PlayerControl robber = Utils.GetPlayerById(robberId); + if (robber == null) continue; + Vector2 currentRobberLocation = robber.GetCustomPosition(); + + // check if duration of trap is completed every second + if (now != LastCheckedRobber) + { + LastCheckedRobber = now; + // If Spike duration finished, reset the speed of trapped player + if (spikeTrigger.ContainsKey(robberId) && now - spikeTrigger[robberId] > CandR_SpikeStripDuration.GetFloat()) + { + if (!captured.ContainsKey(robberId)) + { + Main.AllPlayerSpeed[robberId] = defaultSpeed[robberId]; + if (adrenalineRushActive.Contains(robberId)) + Main.AllPlayerSpeed[robberId] += CandR_AdrenalineRushSpeed.GetFloat(); + robber?.MarkDirtySettings(); + } + spikeTrigger.Remove(robberId); + } + // If flash duration finished, reset the vision of trapped player + if (flashTrigger.ContainsKey(robberId) && now - flashTrigger[robberId] > CandR_FlashBangDuration.GetFloat()) + { + flashTrigger.Remove(robberId); + robber.MarkDirtySettings(); + Logger.Info($"Removed {robberId} from Flash trigger", "Flash remove"); + } + } + + // Check if captured release + if (captured.Any()) + { + foreach ((var captureId, Vector2 capturedLocation) in captured) + { + if (captureId == byte.MaxValue) continue; + PlayerControl capturedPC = Utils.GetPlayerById(captureId); + if (capturedPC == null) continue; + + if (captured.ContainsKey(robberId)) continue; // excluding all the captured players + + float dis = Utils.GetDistance(capturedLocation, currentRobberLocation); + if (dis < 0.3f) + { + var releaseCDforCapture = now - capturedTime[captureId]; + if (releaseCDforCapture < CandR_ReleaseCooldownForCaptured.GetInt()) + { + if (!LastCheckedReleaseCooldownCaptured.ContainsKey(captureId)) LastCheckedReleaseCooldownCaptured[captureId] = now - 1; + if (now != LastCheckedReleaseCooldownCaptured[captureId]) + { + LastCheckedReleaseCooldownCaptured[captureId] = now; + robber.Notify(GetString("C&R_CapturedInReleaseCooldown"), time: CandR_ReleaseCooldownForCaptured.GetInt() - releaseCDforCapture); + Logger.Info($"Time left in release cooldown for captured player, {captureId}: {now - capturedTime[captureId]}", "release canceled"); + } + continue; + } + if (!releaseTime.ContainsKey(robberId)) releaseTime[robberId] = now; + var releaseCDforRobber = now - releaseTime[robberId]; + if (releaseCDforRobber < CandR_ReleaseCooldownForRobber.GetInt()) + { + if (!LastCheckedReleaseCooldownRobber.ContainsKey(robberId)) LastCheckedReleaseCooldownRobber[robberId] = now - 1; + if (now != LastCheckedReleaseCooldownRobber[robberId]) + { + LastCheckedReleaseCooldownRobber[robberId] = now; + robber.Notify(GetString("C&R_RobberInReleaseCooldown"), time: CandR_ReleaseCooldownForRobber.GetInt() - releaseCDforRobber); + Logger.Info($"Time left in release cooldown for robber, {robberId}: {now - releaseTime[robberId]}", "release canceled"); + } + continue; + } + + removeCaptured[captureId] = robberId; + Logger.Info($"to remove captured {captureId}, rob: {robberId}", "C&R FixedUpdate"); + } + } + // remove capture players if possible + if (removeCaptured.Any()) + { + foreach ((byte rescued, byte saviour) in removeCaptured) + { + if (!saved.ContainsKey(saviour)) saved[saviour] = 0; + saved[saviour]++; + Utils.GetPlayerById(rescued).RemoveCaptured(); + SendCandRData(7, playerId: saviour); + releaseTime[saviour] = now; + } + removeCaptured.Clear(); + numCaptures = captured.Count; + SendCandRData(4, capturedCount: numCaptures); + foreach (var pid in cops) + { + var player = Utils.GetPlayerById(pid); + if (player != null) + { + Utils.NotifyRoles(SpecifySeer: player); + } + } + } + } + + if (radar.ContainsKey(robberId)) + { + PlayerControl closest = null; + if (captured.Any()) + { + closest = Main.AllAlivePlayerControls.Where(pc => captured.ContainsKey(pc.PlayerId) && pc != null && pc.PlayerId != robberId) + .MinBy(capturedPC => Utils.GetDistance(robber.GetCustomPosition(), capturedPC.GetCustomPosition())); + } + if (closest == null) + { + closest = Main.AllAlivePlayerControls.Where(pc => pc.Is(CustomRoles.Cop) && pc != null) + .MinBy(closestCop => Utils.GetDistance(robber.GetCustomPosition(), closestCop.GetCustomPosition())); + } + + if (closest != null) + { + if (radar.TryGetValue(robberId, out byte targetId) && targetId != byte.MaxValue) + { + if (targetId != closest.PlayerId) + { + radar[robberId] = closest.PlayerId; + SendCandRData(2, robberId); + Logger.Info($"Set radar for {robberId}, closest: {closest.PlayerId}", "radar Change"); + TargetArrow.Remove(robberId, targetId); + TargetArrow.Add(robberId, closest.PlayerId); + } + } + else + { + radar[robberId] = closest.PlayerId; + SendCandRData(2, robberId); + TargetArrow.Add(robberId, closest.PlayerId); + Logger.Info($"Add radar for {robberId}, closest: {closest.PlayerId}", "radar add"); + } + } + } + + // Check if trap triggered + if (trapLocation.Any()) + { + foreach (KeyValuePair trap in trapLocation) + { + // captured player can not trigger a trap + if (captured.ContainsKey(robberId)) continue; + + // If player already trapped then continue + if (spikeTrigger.ContainsKey(robberId)) continue; + if (flashTrigger.ContainsKey(robberId)) continue; + + var trapDistance = Utils.GetDistance(trap.Key, currentRobberLocation); + // check for spike strip + if (trap.Value is CopAbility.SpikeStrip && trapDistance <= CandR_SpikeStripRadius.GetFloat()) + { + spikeTrigger[robberId] = now; + Main.AllPlayerSpeed[robberId] = Main.MinSpeed; + robber?.MarkDirtySettings(); + removeTrap.Add(trap.Key); // removed the trap from trap location because it was triggered + break; + } + // check for flash bang + if (trap.Value is CopAbility.FlashBang && trapDistance <= CandR_FlashBangRadius.GetFloat()) + { + flashTrigger[robberId] = now; + robber.MarkDirtySettings(); + Logger.Info($"added {robberId} to flashTrigger", "Flash trigger"); + removeTrap.Add(trap.Key); + break; + } + } + if (removeTrap.Any()) + { + foreach (Vector2 removeTrapLoc in removeTrap) + trapLocation.Remove(removeTrapLoc); + } + } + } + + cops.Remove(byte.MaxValue); + foreach (byte copId in cops) + { + if (copId == byte.MaxValue) continue; + PlayerControl copPC = Utils.GetPlayerById(copId); + if (copPC == null) continue; + + if (smokeBombTriggered.Any()) + { + if (now != LastCheckedCop) + { + LastCheckedCop = now; + if (smokeBombTriggered.ContainsKey(copId) && now - smokeBombTriggered[copId] > CandR_SmokeBombDuration.GetFloat()) + { + smokeBombTriggered.Remove(copId); + copPC.MarkDirtySettings(); + Logger.Info($"Removed smoke bomb effect from {copId}", "remove smoke bomb"); + } + } + } + //check for k9 + if (k9.ContainsKey(copId)) + { + PlayerControl closest = Main.AllAlivePlayerControls.Where(pc => pc.Is(CustomRoles.Robber) && !captured.ContainsKey(pc.PlayerId)) + .MinBy(robberPC => Utils.GetDistance(copPC.GetCustomPosition(), robberPC.GetCustomPosition())); + if (closest == null) continue; + if (k9.TryGetValue(copId, out var targetId) && targetId != byte.MaxValue) + { + if (targetId != closest.PlayerId) + { + k9[copId] = closest.PlayerId; + SendCandRData(0, copId); + Logger.Info($"Set k9 for {copId}, closest: {closest.PlayerId}", "Arrow Change"); + TargetArrow.Remove(copId, targetId); + TargetArrow.Add(copId, closest.PlayerId); + } + } + else + { + k9[copId] = closest.PlayerId; + SendCandRData(0, copId); + TargetArrow.Add(copId, closest.PlayerId); + Logger.Info($"Add k9 for {copId}, closest: {closest.PlayerId}", "Arrow Change"); + } + } + } + } + } +} diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 3ad10f27c..8ae5600cd 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -18,6 +18,9 @@ public static class CustomRolesHelper public static readonly Custom_Team[] AllRoleTypes = EnumHelper.GetAllValues(); public static CustomRoles GetVNRole(this CustomRoles role) // RoleBase: Impostor, Shapeshifter, Crewmate, Engineer, Scientist { + //C&R + if (Options.CurrentGameMode is CustomGameMode.CandR && role is CustomRoles.Robber) return CustomRoles.Engineer; + // Vanilla roles if (role.IsVanilla()) return role; @@ -39,7 +42,15 @@ public static CustomRoles GetVNRole(this CustomRoles role) // RoleBase: Impostor public static RoleTypes GetDYRole(this CustomRoles role) // Role has a kill button (Non-Impostor) { - if (role is CustomRoles.Killer) return RoleTypes.Impostor; // FFA + switch (Options.CurrentGameMode) + { + case CustomGameMode.FFA: //FFA + if (role is CustomRoles.Killer) return RoleTypes.Impostor; + break; + case CustomGameMode.CandR: //C&R + if (role is CustomRoles.Cop) return RoleTypes.Shapeshifter; + break; + } return (role.GetStaticRoleClass().ThisRoleBase is CustomRoles.Impostor or CustomRoles.Shapeshifter) && !role.IsImpostor() ? role.GetStaticRoleClass().ThisRoleBase.GetRoleTypes() @@ -66,6 +77,7 @@ public static bool HasImpKillButton(this PlayerControl player, bool considerVani // this function now always uses current mod role to decide kill button access? if (player == null) return false; + if (Options.CurrentGameMode is CustomGameMode.CandR && player.Is(CustomRoles.Cop)) return true; var customRole = player.GetCustomRole(); return customRole.GetDYRole() == RoleTypes.Impostor || customRole.GetVNRole() is CustomRoles.Impostor or CustomRoles.Shapeshifter or CustomRoles.Phantom; } diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 4ff7675aa..f57214702 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -1060,6 +1060,11 @@ public static string GetRealName(this PlayerControl player, bool isMeeting = fal public static bool CanUseKillButton(this PlayerControl pc) { if (GameStates.IsLobby) return false; + if (Options.CurrentGameMode is CustomGameMode.CandR) //C&R + { + + return (pc.Is(CustomRoles.Cop)); + } if (!pc.IsAlive() || Pelican.IsEaten(pc.PlayerId) || DollMaster.IsDoll(pc.PlayerId)) return false; if (pc.GetClient().GetHashedPuid() == Main.FirstDiedPrevious && !Options.ShieldedCanUseKillButton.GetBool() && MeetingStates.FirstMeeting) return false; if (pc.Is(CustomRoles.Killer) || Mastermind.PlayerIsManipulated(pc)) return true; @@ -1087,13 +1092,19 @@ public static bool HasKillButton(this PlayerControl pc) _ => false }; } - public static bool CanUseVents(this PlayerControl player) => player != null && (player.CanUseImpostorVentButton() || player.GetCustomRole().GetVNRole() == CustomRoles.Engineer); + public static bool CanUseVents(this PlayerControl player) => Options.CurrentGameMode switch + { + CustomGameMode.CandR => player.Is(CustomRoles.Robber) && !CopsAndRobbersManager.captured.ContainsKey(player.PlayerId), + _ => player != null && (player.CanUseImpostorVentButton() || player.GetCustomRole().GetVNRole() == CustomRoles.Engineer) + }; + public static bool CantUseVent(this PlayerControl player, int ventId) => player == null || !player.CanUseVents() || (CustomRoleManager.BlockedVentsList.TryGetValue(player.PlayerId, out var blockedVents) && blockedVents.Contains(ventId)); public static bool HasAnyBlockedVent(this PlayerControl player) => player != null && CustomRoleManager.BlockedVentsList.TryGetValue(player.PlayerId, out var blockedVents) && blockedVents.Any(); public static bool NotUnlockVent(this PlayerControl player, int ventId) => player != null && CustomRoleManager.DoNotUnlockVentsList.TryGetValue(player.PlayerId, out var blockedVents) && blockedVents.Contains(ventId); public static bool CanUseImpostorVentButton(this PlayerControl pc) { + if (Options.CurrentGameMode is CustomGameMode.CandR) return false; if (!pc.IsAlive()) return false; if (GameStates.IsHideNSeek) return true; if (pc.Is(CustomRoles.Killer) || pc.Is(CustomRoles.Nimble)) return true; @@ -1108,6 +1119,7 @@ public static bool CanUseImpostorVentButton(this PlayerControl pc) } public static bool CanUseSabotage(this PlayerControl pc) { + if (Options.CurrentGameMode is CustomGameMode.CandR) return false; if (pc.Is(Custom_Team.Impostor) && !pc.IsAlive() && Options.DeadImpCantSabotage.GetBool()) return false; var playerRoleClass = pc.GetRoleClass(); @@ -1120,44 +1132,53 @@ public static void ResetKillCooldown(this PlayerControl player) Main.AllPlayerKillCooldown[player.PlayerId] = Options.DefaultKillCooldown; // FFA - if (player.Is(CustomRoles.Killer)) + switch (Options.CurrentGameMode) { - Main.AllPlayerKillCooldown[player.PlayerId] = FFAManager.FFA_KCD.GetFloat(); - } - else - { - player.GetRoleClass()?.SetKillCooldown(player.PlayerId); - } + case CustomGameMode.FFA: + if (player.Is(CustomRoles.Killer)) + { + Main.AllPlayerKillCooldown[player.PlayerId] = FFAManager.FFA_KCD.GetFloat(); + } + break; + case CustomGameMode.CandR: + if (player.Is(CustomRoles.Cop)) + CopsAndRobbersManager.CaptureCooldown(player); + break; - var playerSubRoles = player.GetCustomSubRoles(); + default: + player.GetRoleClass()?.SetKillCooldown(player.PlayerId); - if (playerSubRoles.Any()) - foreach (var subRole in playerSubRoles) - { - switch (subRole) - { - case CustomRoles.LastImpostor when player.PlayerId == LastImpostor.currentId: - LastImpostor.SetKillCooldown(); - break; - case CustomRoles.Mare: - Main.AllPlayerKillCooldown[player.PlayerId] = Mare.KillCooldownInLightsOut.GetFloat(); - break; + var playerSubRoles = player.GetCustomSubRoles(); - case CustomRoles.Overclocked: - Main.AllPlayerKillCooldown[player.PlayerId] -= Main.AllPlayerKillCooldown[player.PlayerId] * (Overclocked.OverclockedReduction.GetFloat() / 100); - break; + if (playerSubRoles.Any()) + foreach (var subRole in playerSubRoles) + { + switch (subRole) + { + case CustomRoles.LastImpostor when player.PlayerId == LastImpostor.currentId: + LastImpostor.SetKillCooldown(); + break; - case CustomRoles.Diseased: - Diseased.IncreaseKCD(player); - break; + case CustomRoles.Mare: + Main.AllPlayerKillCooldown[player.PlayerId] = Mare.KillCooldownInLightsOut.GetFloat(); + break; - case CustomRoles.Antidote: - Antidote.ReduceKCD(player); - break; - } - } + case CustomRoles.Overclocked: + Main.AllPlayerKillCooldown[player.PlayerId] -= Main.AllPlayerKillCooldown[player.PlayerId] * (Overclocked.OverclockedReduction.GetFloat() / 100); + break; + + case CustomRoles.Diseased: + Diseased.IncreaseKCD(player); + break; + case CustomRoles.Antidote: + Antidote.ReduceKCD(player); + break; + } + } + break; + } if (!player.HasImpKillButton(considerVanillaShift: false)) Main.AllPlayerKillCooldown[player.PlayerId] = 300f; diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index 77b422d3b..b0d610377 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -82,20 +82,26 @@ public override IGameOptions BuildGameOptions() opt.BlackOut(state.IsBlackOut); CustomRoles role = player.GetCustomRole(); - if (Options.CurrentGameMode == CustomGameMode.FFA) + switch (Options.CurrentGameMode) { - if (FFAManager.FFALowerVisionList.ContainsKey(player.PlayerId)) - { - opt.SetVision(true); - opt.SetFloat(FloatOptionNames.CrewLightMod, FFAManager.FFA_LowerVision.GetFloat()); - opt.SetFloat(FloatOptionNames.ImpostorLightMod, FFAManager.FFA_LowerVision.GetFloat()); - } - else - { - opt.SetVision(true); - opt.SetFloat(FloatOptionNames.CrewLightMod, 1.25f); - opt.SetFloat(FloatOptionNames.ImpostorLightMod, 1.25f); - } + case CustomGameMode.FFA: + if (FFAManager.FFALowerVisionList.ContainsKey(player.PlayerId)) + { + opt.SetVision(true); + opt.SetFloat(FloatOptionNames.CrewLightMod, FFAManager.FFA_LowerVision.GetFloat()); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, FFAManager.FFA_LowerVision.GetFloat()); + } + else + { + opt.SetVision(true); + opt.SetFloat(FloatOptionNames.CrewLightMod, 1.25f); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, 1.25f); + } + break; + + case CustomGameMode.CandR: + CopsAndRobbersManager.ApplyGameOptions(ref opt, player); + break; } if (player.Is(Custom_Team.Impostor)) @@ -126,7 +132,7 @@ public override IGameOptions BuildGameOptions() state.taskState.hasTasks = Utils.HasTasks(player.Data, false); - if (Main.UnShapeShifter.Contains(player.PlayerId)) + if (Main.UnShapeShifter.Contains(player.PlayerId) && Options.CurrentGameMode != CustomGameMode.CandR) { AURoleOptions.ShapeshifterDuration = 1f; } diff --git a/Modules/ModUpdater.cs b/Modules/ModUpdater.cs index 3fb07edf2..a9689c708 100644 --- a/Modules/ModUpdater.cs +++ b/Modules/ModUpdater.cs @@ -17,7 +17,8 @@ namespace TOHE; public class ModUpdater { //private static readonly string URL_2018k = "http://api.tohre.dev"; - private static readonly string URL_Github = "https://api.github.com/repos/0xDrMoe/TownofHost-Enhanced"; + //private static readonly string URL_Github = "https://api.github.com/repos/0xDrMoe/TownofHost-Enhanced"; + private static readonly string URL_Github = "https://api.github.com/repos/EnhancedNetwork/TownofHost-Enhanced"; //public static readonly string downloadTest = "https://github.com/Pietrodjaowjao/TOHEN-Contributions/releases/download/v123123123/TOHE.dll"; public static bool hasUpdate = false; //public static bool isNewer = false; diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index 6d21f723f..f05067a42 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -13,6 +13,7 @@ public enum CustomGameMode { Standard = 0x01, FFA = 0x02, + CandR = 0x03, HidenSeekTOHE = 0x08, // HidenSeekTOHE must be after other game modes All = int.MaxValue @@ -49,13 +50,26 @@ public static CustomGameMode CurrentGameMode => GameMode.GetInt() switch { 1 => CustomGameMode.FFA, - 2 => CustomGameMode.HidenSeekTOHE, // HidenSeekTOHE must be after other game modes + 2 => CustomGameMode.CandR, + 3 => CustomGameMode.HidenSeekTOHE, // HidenSeekTOHE must be after other game modes _ => CustomGameMode.Standard }; + + public static int GetGameModeInt(CustomGameMode mode) + => mode switch + { + CustomGameMode.FFA => 1, + CustomGameMode.CandR => 2, + CustomGameMode.HidenSeekTOHE => 3, // HidenSeekTOHE must be after other game modes + _ => 0 + }; + public static readonly string[] gameModes = [ "Standard", "FFA", + "C&R", + "Hide&SeekTOHE", // HidenSeekTOHE must be after other game modes @@ -1007,13 +1021,15 @@ private static System.Collections.IEnumerator CoLoadOptions() GradientTagsOpt = BooleanOptionItem.Create(60031, "EnableGadientTags", false, TabGroup.SystemSettings, false) .SetHeader(true); EnableKillerLeftCommand = BooleanOptionItem.Create(60040, "EnableKillerLeftCommand", true, TabGroup.SystemSettings, false) - .HideInHnS(); + .HideInHnS() + .HideInCandR(); ShowMadmatesInLeftCommand = BooleanOptionItem.Create(60042, "ShowMadmatesInLeftCommand", true, TabGroup.SystemSettings, false) .SetParent(EnableKillerLeftCommand); ShowApocalypseInLeftCommand = BooleanOptionItem.Create(60043, "ShowApocalypseInLeftCommand", true, TabGroup.SystemSettings, false) .SetParent(EnableKillerLeftCommand); SeeEjectedRolesInMeeting = BooleanOptionItem.Create(60041, "SeeEjectedRolesInMeeting", true, TabGroup.SystemSettings, false) - .HideInHnS(); + .HideInHnS() + .HideInCandR(); KickLowLevelPlayer = IntegerOptionItem.Create(60050, "KickLowLevelPlayer", new(0, 100, 1), 0, TabGroup.SystemSettings, false) .SetValueFormat(OptionFormat.Level) @@ -1103,9 +1119,11 @@ private static System.Collections.IEnumerator CoLoadOptions() .SetColor(Color.blue); HideExileChat = BooleanOptionItem.Create(60292, "HideExileChat", true, TabGroup.SystemSettings, false) .SetColor(Color.blue) - .HideInHnS(); + .HideInHnS() + .HideInCandR(); RemovePetsAtDeadPlayers = BooleanOptionItem.Create(60294, "RemovePetsAtDeadPlayers", false, TabGroup.SystemSettings, false) - .SetColor(Color.magenta); + .SetColor(Color.magenta) + .HideInCandR(); CheatResponses = StringOptionItem.Create(60250, "CheatResponses", CheatResponsesName, 0, TabGroup.SystemSettings, false) .SetHeader(true); @@ -1167,6 +1185,9 @@ private static System.Collections.IEnumerator CoLoadOptions() //FFA FFAManager.SetupCustomOption(); + //C&R + CopsAndRobbersManager.SetupCustomOption(); + // Hide & Seek TextOptionItem.Create(10000055, "MenuTitle.Hide&Seek", TabGroup.ModSettings) .SetGameMode(CustomGameMode.HidenSeekTOHE) @@ -1799,11 +1820,13 @@ private static System.Collections.IEnumerator CoLoadOptions() // 其它设定 TextOptionItem.Create(10000031, "MenuTitle.Other", TabGroup.ModSettings) .HideInFFA() + .HideInCandR() .SetColor(new Color32(193, 255, 209, byte.MaxValue)); // 梯子摔死 LadderDeath = BooleanOptionItem.Create(60760, "LadderDeath", false, TabGroup.ModSettings, false) .SetColor(new Color32(193, 255, 209, byte.MaxValue)) - .HideInFFA(); + .HideInFFA() + .HideInCandR(); LadderDeathChance = StringOptionItem.Create(60761, "LadderDeathChance", EnumHelper.GetAllNames()[1..], 0, TabGroup.ModSettings, false) .SetParent(LadderDeath); diff --git a/Modules/OptionItem/OptionItem.cs b/Modules/OptionItem/OptionItem.cs index 66fdbb4b4..9cc692fa6 100644 --- a/Modules/OptionItem/OptionItem.cs +++ b/Modules/OptionItem/OptionItem.cs @@ -28,6 +28,7 @@ public abstract class OptionItem public OptionFormat ValueFormat { get; protected set; } public CustomGameMode GameMode { get; protected set; } public CustomGameMode HideOptionInFFA { get; protected set; } + public CustomGameMode HideOptionInCandR { get; protected set; } public CustomGameMode HideOptionInHnS { get; protected set; } public bool IsHeader { get; protected set; } public bool IsHidden { get; protected set; } @@ -76,6 +77,7 @@ public OptionItem(int id, string name, int defaultValue, TabGroup tab, bool isSi ValueFormat = OptionFormat.None; GameMode = CustomGameMode.All; HideOptionInFFA = CustomGameMode.All; + HideOptionInCandR = CustomGameMode.All; HideOptionInHnS = CustomGameMode.All; IsHeader = false; IsHidden = false; @@ -130,6 +132,7 @@ public OptionItem Do(Action action) public OptionItem SetHidden(bool value) => Do(i => i.IsHidden = value); public OptionItem SetText(bool value) => Do(i => i.IsText = value); public OptionItem HideInFFA(CustomGameMode value = CustomGameMode.FFA) => Do(i => i.HideOptionInFFA = value); + public OptionItem HideInCandR(CustomGameMode value = CustomGameMode.CandR) => Do(i => i.HideOptionInCandR = value); //C&R public OptionItem HideInHnS(CustomGameMode value = CustomGameMode.HidenSeekTOHE) => Do(i => i.HideOptionInHnS = value); public OptionItem SetParent(OptionItem parent) => Do(i => @@ -181,7 +184,7 @@ public virtual string GetString() // Deprecated IsHidden function public virtual bool IsHiddenOn(CustomGameMode mode) { - return IsHidden || this.Parent?.IsHiddenOn(Options.CurrentGameMode) == true || (HideOptionInFFA != CustomGameMode.All && HideOptionInFFA == mode) || (HideOptionInHnS != CustomGameMode.All && HideOptionInHnS == mode) || (GameMode != CustomGameMode.All && GameMode != mode); + return IsHidden || this.Parent?.IsHiddenOn(Options.CurrentGameMode) == true || (HideOptionInCandR != CustomGameMode.All && HideOptionInCandR == mode) || (HideOptionInFFA != CustomGameMode.All && HideOptionInFFA == mode) || (HideOptionInHnS != CustomGameMode.All && HideOptionInHnS == mode) || (GameMode != CustomGameMode.All && GameMode != mode); } public string ApplyFormat(string value) { diff --git a/Modules/RPC.cs b/Modules/RPC.cs index 776dffd82..3ce3011cf 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -118,6 +118,7 @@ public enum CustomRPC : byte // 185/255 USED //FFA SyncFFAPlayer, SyncFFANameNotify, + SyncCandRData, } [Obfuscation(Exclude = true)] public enum Sounds @@ -552,6 +553,9 @@ public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)] byte c case CustomRPC.SyncFFAPlayer: FFAManager.ReceiveRPCSyncFFAPlayer(reader); break; + case CustomRPC.SyncCandRData: + CopsAndRobbersManager.ReceiveCandRData(reader); + break; case CustomRPC.SyncAllPlayerNames: Main.AllPlayerNames.Clear(); Main.AllClientRealNames.Clear(); diff --git a/Modules/Utils.cs b/Modules/Utils.cs index dd650a867..4df2ca374 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -565,9 +565,19 @@ public static string GetKillCountText(byte playerId, bool ffa = false) return ColorString(new Color32(255, 69, 0, byte.MaxValue), string.Format(GetString("KillCount"), count)); } public static string GetVitalText(byte playerId, bool RealKillerColor = false) - { + { var state = Main.PlayerStates[playerId]; string deathReason = state.IsDead ? state.deathReason == PlayerState.DeathReason.etc && state.Disconnected ? GetString("Disconnected") : GetString("DeathReason." + state.deathReason) : GetString("Alive"); + if (Options.CurrentGameMode is CustomGameMode.CandR) + { + if (deathReason == GetString("Alive")) + { + if (CopsAndRobbersManager.captured.ContainsKey(playerId)) + deathReason = ColorString(GetRoleColor(CustomRoles.Robber), GetString("Captured")); + else deathReason = ColorString(Color.cyan, deathReason); + } + return deathReason; + } if (RealKillerColor) { var KillerId = state.GetRealKiller(); @@ -609,6 +619,7 @@ public static bool HasTasks(NetworkedPlayerInfo playerData, bool ForRecompute = if (GameStates.IsHideNSeek) return hasTasks; var role = States.MainRole; + if (Options.CurrentGameMode == CustomGameMode.CandR) return CopsAndRobbersManager.HasTasks(role); //C&R if (States.RoleClass != null && States.RoleClass.HasTasks(playerData, role, ForRecompute) == false) hasTasks = false; @@ -679,41 +690,46 @@ public static string GetProgressText(byte playerId, bool comms = false) if (!Main.playerVersion.ContainsKey(AmongUsClient.Instance.HostId)) return string.Empty; var ProgressText = new StringBuilder(); var role = Main.PlayerStates[playerId].MainRole; - - if (Options.CurrentGameMode == CustomGameMode.FFA && role == CustomRoles.Killer) + + switch (Options.CurrentGameMode) { - ProgressText.Append(FFAManager.GetDisplayScore(playerId)); - } - else - { - ProgressText.Append(playerId.GetRoleClassById()?.GetProgressText(playerId, comms)); + case CustomGameMode.FFA: + if (role is CustomRoles.Killer) + ProgressText.Append(FFAManager.GetDisplayScore(playerId)); + break; + case CustomGameMode.CandR: + ProgressText.Append(CopsAndRobbersManager.GetProgressText(playerId)); + break; + default: + ProgressText.Append(playerId.GetRoleClassById()?.GetProgressText(playerId, comms)); - if (ProgressText.Length == 0) - { - var taskState = Main.PlayerStates?[playerId].TaskState; - if (taskState.hasTasks) + if (ProgressText.Length == 0) { - Color TextColor; - var info = GetPlayerInfoById(playerId); - var TaskCompleteColor = HasTasks(info) ? Color.green : GetRoleColor(role).ShadeColor(0.5f); - var NonCompleteColor = HasTasks(info) ? Color.yellow : Color.white; + var taskState = Main.PlayerStates?[playerId].TaskState; + if (taskState.hasTasks) + { + Color TextColor; + var info = GetPlayerInfoById(playerId); + var TaskCompleteColor = HasTasks(info) ? Color.green : GetRoleColor(role).ShadeColor(0.5f); + var NonCompleteColor = HasTasks(info) ? Color.yellow : Color.white; - if (Workhorse.IsThisRole(playerId)) - NonCompleteColor = Workhorse.RoleColor; + if (Workhorse.IsThisRole(playerId)) + NonCompleteColor = Workhorse.RoleColor; - var NormalColor = taskState.IsTaskFinished ? TaskCompleteColor : NonCompleteColor; - if (Main.PlayerStates.TryGetValue(playerId, out var ps) && ps.MainRole == CustomRoles.Crewpostor) - NormalColor = Color.red; + var NormalColor = taskState.IsTaskFinished ? TaskCompleteColor : NonCompleteColor; + if (Main.PlayerStates.TryGetValue(playerId, out var ps) && ps.MainRole == CustomRoles.Crewpostor) + NormalColor = Color.red; - TextColor = comms ? Color.gray : NormalColor; - string Completed = comms ? "?" : $"{taskState.CompletedTasksCount}"; - ProgressText.Append(ColorString(TextColor, $" ({Completed}/{taskState.AllTasksCount})")); + TextColor = comms ? Color.gray : NormalColor; + string Completed = comms ? "?" : $"{taskState.CompletedTasksCount}"; + ProgressText.Append(ColorString(TextColor, $" ({Completed}/{taskState.AllTasksCount})")); + } } - } - else - { - ProgressText.Insert(0, " "); - } + else + { + ProgressText.Insert(0, " "); + } + break; } return ProgressText.ToString(); } @@ -1626,6 +1642,8 @@ void SetRealName() } if (Options.CurrentGameMode == CustomGameMode.FFA) name = $"{GetString("ModeFFA")}\r\n" + name; + else if (Options.CurrentGameMode == CustomGameMode.CandR) + name = $"{GetString("ModeC&R")}\r\n" + name; } var modtag = ""; @@ -1986,6 +2004,9 @@ public static Task DoNotifyRoles(PlayerControl SpecifySeer = null, PlayerControl case CustomGameMode.FFA: SelfSuffix.Append(FFAManager.GetPlayerArrow(seer)); break; + case CustomGameMode.CandR: + SelfSuffix.Append(CopsAndRobbersManager.GetClosestArrow(seer, seer)); + break; } @@ -2503,6 +2524,8 @@ public static void DumpLog() public static string SummaryTexts(byte id, bool disableColor = true, bool check = false) { + if (Options.CurrentGameMode is CustomGameMode.CandR) return CopsAndRobbersManager.SummaryTexts(id, disableColor, check); + string name; try { @@ -2515,7 +2538,6 @@ public static string SummaryTexts(byte id, bool disableColor = true, bool check name = Main.AllPlayerNames[id].RemoveHtmlTags().Replace("\r\n", string.Empty) ?? "ERROR"; } - var taskState = Main.PlayerStates?[id].TaskState; // Impossible to output summarytexts for a player without playerState diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index 60c0b2ea2..a9f055285 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -1579,6 +1579,10 @@ static Color32 RndCLR() Utils.SendMessage(string.Format(GetString("StartCommandStarted"), PlayerControl.LocalPlayer.name)); Logger.Info("Game Starting", "ChatCommand"); break; + case "/ability": + subArgs = args.Length < 2 ? "" : args[1]; + CopsAndRobbersManager.AbilityDescription(subArgs); + break; default: Main.isChatCommand = false; @@ -1971,13 +1975,51 @@ public static bool GetRoleByName(string name, out CustomRoles role) } public static void SendRolesInfo(string role, byte playerId, bool isDev = false, bool isUp = false) { - if (Options.CurrentGameMode == CustomGameMode.FFA) - { - Utils.SendMessage(GetString("ModeDescribe.FFA"), playerId); - return; - } role = role.Trim().ToLower(); if (role.StartsWith("/r")) _ = role.Replace("/r", string.Empty); + switch (Options.CurrentGameMode) + { + case CustomGameMode.FFA: + Utils.SendMessage(GetString("ModeDescribe.FFA"), playerId); + return; + case CustomGameMode.CandR: + var copName = GetString(CustomRoles.Cop.ToString()).ToLower().Trim().TrimStart('*').Replace(" ", string.Empty); + var robberName = GetString(CustomRoles.Robber.ToString()).ToLower().Trim().TrimStart('*').Replace(" ", string.Empty); + var Conf1 = new StringBuilder(); + + CustomRoles rl1; + if (role == copName) rl1 = CustomRoles.Cop; + else if (role == robberName) rl1 = CustomRoles.Robber; + else + { + Utils.SendMessage(GetString("ModeDescribe.C&R"), playerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Cop), GetString("ModeC&R"))); + return; + } + + var description = rl1.GetInfoLong(); + var title1 = Utils.ColorString(Utils.GetRoleColor(rl1), GetString($"{rl1}")); + string rlHex1 = Utils.GetRoleColorCode(rl1); + var Setting = $"{GetString(rl1.ToString())} {GetString("Settings:")}\n"; + Conf1.Clear().Append(Setting); + + foreach (OptionItem opt in CopsAndRobbersManager.roleSettings[rl1]) + { + Conf1.Append($"{opt.GetName(true)}: {opt.GetString()}\n"); + int deep = 0; + if (opt.Children.Any()) deep = 1; + Utils.ShowChildrenSettings(opt, ref Conf1, deep: deep); + var cleared = Conf1.ToString(); + Conf1.Clear().Append($"{cleared}"); + } + Conf1.Append(""); + + // Show role info + Utils.SendMessage(description, playerId, title1, noReplay: true); + + // Show role settings + Utils.SendMessage("", playerId, Conf1.ToString(), noReplay: true); + return; + } if (role.StartsWith("/up")) _ = role.Replace("/up", string.Empty); if (role.EndsWith("\r\n")) _ = role.Replace("\r\n", string.Empty); if (role.EndsWith("\n")) _ = role.Replace("\n", string.Empty); @@ -3367,8 +3409,11 @@ public static void OnReceiveChat(PlayerControl player, string text, out bool can GameStartManager.Instance.countDownTimer = countdown; Utils.SendMessage(string.Format(GetString("StartCommandStarted"), player.name)); break; - - + case "/ability": + subArgs = args.Length < 2 ? "" : args[1]; + CopsAndRobbersManager.AbilityDescription(subArgs, player.PlayerId); + break; + default: if (SpamManager.CheckSpam(player, text)) return; break; @@ -3509,4 +3554,4 @@ public static bool Prefix(PlayerControl __instance, string chatText, ref bool __ __result = true; return false; } -} +} \ No newline at end of file diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index 34ace6400..98a16c81f 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -54,9 +54,12 @@ public static bool Prefix() predicate.CheckForEndGame(out reason); // FFA - if (Options.CurrentGameMode == CustomGameMode.FFA) + switch (Options.CurrentGameMode) { - if (WinnerIds.Count > 0 || WinnerTeam != CustomWinner.Default) + case CustomGameMode.FFA: + case CustomGameMode.CandR: + + if (WinnerIds.Count > 0 || WinnerTeam != CustomWinner.Default) { ShipStatus.Instance.enabled = false; StartEndGame(reason); @@ -538,6 +541,8 @@ void SetGhostRole(bool ToGhostImpostor) public static void SetPredicateToNormal() => predicate = new NormalGameEndPredicate(); public static void SetPredicateToFFA() => predicate = new FFAGameEndPredicate(); + public static void SetPredicateToCandR() => predicate = new CandRGameEndPredicate(); //C&R + // ===== Check Game End ===== @@ -656,6 +661,91 @@ public static bool CheckGameEndByLivingPlayers(out GameOverReason reason) } } +// For C&R +class CandRGameEndPredicate : GameEndPredicate +{ + public override bool CheckForEndGame(out GameOverReason reason) + { + // task win + reason = GameOverReason.ImpostorByKill; + if (WinnerTeam != CustomWinner.Default) return false; + if (CheckGameEndByLivingPlayers(out reason) || CheckGameEndByTask(out reason)) return true; + return false; + } + public static bool CheckGameEndByLivingPlayers(out GameOverReason reason) + { + + // everyone died + reason = GameOverReason.ImpostorByKill; + + if (CopsAndRobbersManager.RoundTime <= 0) + { + reason = GameOverReason.HideAndSeek_ByTimer; + ResetAndSetWinner(CustomWinner.Cops); + WinnerIds = [.. CopsAndRobbersManager.cops]; + Logger.Warn("Game end because round time finished", "C&R"); + return true; + } + + if (!Main.AllAlivePlayerControls.Any()) + { + reason = GameOverReason.ImpostorByKill; + ResetAndSetWinner(CustomWinner.None); + Logger.Info("Game end because all players dead", "C&R"); + return true; + } + + bool copsAlive = false; + + + bool allCaptured = true; + foreach (var pc in Main.AllAlivePlayerControls) + { + if (copsAlive && !allCaptured) break; + if (pc.Is(CustomRoles.Cop)) copsAlive = true; + else if (pc.Is(CustomRoles.Robber) && !CopsAndRobbersManager.captured.ContainsKey(pc.PlayerId)) allCaptured = false; + } + + // no cops left + if (!copsAlive) + { + reason = GameOverReason.ImpostorDisconnect; + ResetAndSetWinner(CustomWinner.Robbers); + WinnerIds = [.. CopsAndRobbersManager.robbers]; + Logger.Info("Game end because No cops left", "C&R"); + return true; + } + + // all robbers captured + if (allCaptured) + { + reason = GameOverReason.ImpostorByKill; + ResetAndSetWinner(CustomWinner.Cops); + WinnerIds = [.. CopsAndRobbersManager.cops]; + Logger.Info("Game end because all robbers captured", "C&R"); + return true; + } + + return false; + } + + public override bool CheckGameEndByTask(out GameOverReason reason) + { + reason = GameOverReason.ImpostorByKill; + + if (GameData.Instance.TotalTasks <= GameData.Instance.CompletedTasks) + { + reason = GameOverReason.HumansByTask; + ResetAndSetWinner(CustomWinner.Robbers); + WinnerIds = [.. CopsAndRobbersManager.robbers]; + Logger.Info("Game end because robbers completed all tasks", "C&R"); + return true; + } + return false; + } +} + + // For FFA Games class FFAGameEndPredicate : GameEndPredicate { diff --git a/Patches/GameOptionsMenuPatch.cs b/Patches/GameOptionsMenuPatch.cs index 6703bb06e..ea6247be6 100644 --- a/Patches/GameOptionsMenuPatch.cs +++ b/Patches/GameOptionsMenuPatch.cs @@ -692,13 +692,13 @@ private static bool UpdateValuePrefix(StringOption __instance) if (item is PresetOptionItem || (item is StringOptionItem && item.Name == "GameMode")) { - if (Options.GameMode.GetInt() == 2 && !GameStates.IsHideNSeek) //Hide And Seek + if (Options.CurrentGameMode == CustomGameMode.HidenSeekTOHE && !GameStates.IsHideNSeek) //Hide And Seek { Options.GameMode.SetValue(0); } - else if (Options.GameMode.GetInt() != 2 && GameStates.IsHideNSeek) + else if (Options.CurrentGameMode == CustomGameMode.HidenSeekTOHE && GameStates.IsHideNSeek) { - Options.GameMode.SetValue(2); + Options.GameMode.SetValue(Options.GetGameModeInt(CustomGameMode.HidenSeekTOHE)); } GameOptionsMenuPatch.ReOpenSettings(item.Name != "GameMode" ? 1 : 4); } diff --git a/Patches/GameSettingMenuPatch.cs b/Patches/GameSettingMenuPatch.cs index e7a53796c..1d58e0914 100644 --- a/Patches/GameSettingMenuPatch.cs +++ b/Patches/GameSettingMenuPatch.cs @@ -32,6 +32,7 @@ public static void StartPostfix(GameSettingMenu __instance) { CustomGameMode.HidenSeekTOHE => Enum.GetValues().Skip(3).ToArray(), CustomGameMode.FFA => Enum.GetValues().Skip(2).ToArray(), + CustomGameMode.CandR => Enum.GetValues().Skip(3).ToArray(), _ => [] }; diff --git a/Patches/HudPatch.cs b/Patches/HudPatch.cs index 93783394b..225df8b45 100644 --- a/Patches/HudPatch.cs +++ b/Patches/HudPatch.cs @@ -71,6 +71,27 @@ public static void Postfix(HudManager __instance) } LowerInfoText.text = FFAManager.GetHudText(); } + else if (Options.CurrentGameMode == CustomGameMode.CandR) + { + if (LowerInfoText == null) + { + TempLowerInfoText = new GameObject("CountdownText"); + TempLowerInfoText.transform.position = new Vector3(0f, -2f, 1f); + LowerInfoText = TempLowerInfoText.AddComponent(); + //LowerInfoText.text = string.Format(GetString("CountdownText")); + LowerInfoText.alignment = TextAlignmentOptions.Center; + //LowerInfoText = Object.Instantiate(__instance.KillButton.buttonLabelText); + LowerInfoText.transform.parent = __instance.transform; + LowerInfoText.transform.localPosition = new Vector3(0, -2f, 0); + LowerInfoText.overflowMode = TextOverflowModes.Overflow; + LowerInfoText.enableWordWrapping = false; + LowerInfoText.color = Color.white; + LowerInfoText.outlineColor = Color.black; + LowerInfoText.outlineWidth = 20000000f; + LowerInfoText.fontSize = 2f; + } + LowerInfoText.text = CopsAndRobbersManager.GetHudText(); + } if (player.IsAlive()) { // Set default @@ -80,6 +101,9 @@ public static void Postfix(HudManager __instance) player.GetRoleClass()?.SetAbilityButtonText(__instance, player.PlayerId); + if (Options.CurrentGameMode is CustomGameMode.CandR) + CopsAndRobbersManager.SetAbilityButtonText(__instance, player.PlayerId); + // Set lower info text for modded players if (LowerInfoText == null) { @@ -342,6 +366,24 @@ public static void Postfix(TaskPanelBehaviour __instance) AllText = $"{AllText}"; + break; + case CustomGameMode.CandR: //C&R + var lines1 = taskText.Split("\r\n\n")[0].Split("\r\n\n")[0].Split("\r\n"); + StringBuilder sb1 = new(); + foreach (var eachLine in lines1) + { + var line = eachLine.Trim(); + if ((line.StartsWith("") || line.StartsWith("")) && sb1.Length < 1 && !line.Contains('(')) continue; + sb1.Append(line + "\r\n"); + } + + if (sb1.Length > 1) + { + var text = sb1.ToString().TrimEnd('\n').TrimEnd('\r'); + if (!Utils.HasTasks(player.Data, false) && sb1.ToString().Count(s => (s == '\n')) >= 2) + text = $"{Utils.ColorString(Utils.GetRoleColor(player.GetCustomRole()).ShadeColor(0.2f), GetString("FakeTask"))}\r\n{text}"; + AllText += $"\r\n\r\n{text}"; + } break; } diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index 8b0ed6b72..ecc8f0ffd 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -73,6 +73,7 @@ public static void Prefix() RPC.RpcVersionCheck(); FFAManager.SetData(); + CopsAndRobbersManager.SetData(); } } [HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.ShowRole))] @@ -122,30 +123,38 @@ public static void Postfix(IntroCutscene __instance) { PlayerControl localPlayer = PlayerControl.LocalPlayer; CustomRoles role = localPlayer.GetCustomRole(); - if (Options.CurrentGameMode == CustomGameMode.FFA) + switch (Options.CurrentGameMode) { - var color = ColorUtility.TryParseHtmlString("#00ffff", out var c) ? c : new(255, 255, 255, 255); - __instance.YouAreText.transform.gameObject.SetActive(false); - __instance.RoleText.text = "FREE FOR ALL"; - __instance.RoleText.color = color; - __instance.RoleBlurbText.color = color; - __instance.RoleBlurbText.text = "KILL EVERYONE TO WIN"; - } - else - { - if (!role.IsVanilla()) - { + case CustomGameMode.FFA: + var color = ColorUtility.TryParseHtmlString("#00ffff", out var c) ? c : new(255, 255, 255, 255); + __instance.YouAreText.transform.gameObject.SetActive(false); + __instance.RoleText.text = GetString("FFA"); + __instance.RoleText.color = color; + __instance.RoleBlurbText.color = color; + __instance.RoleBlurbText.text = GetString("KillerInfo"); + break; + case CustomGameMode.CandR: //C&R __instance.YouAreText.color = Utils.GetRoleColor(role); __instance.RoleText.text = Utils.GetRoleName(role); __instance.RoleText.color = Utils.GetRoleColor(role); __instance.RoleBlurbText.color = Utils.GetRoleColor(role); __instance.RoleBlurbText.text = localPlayer.GetRoleInfo(); - } + break; + default: + if (!role.IsVanilla()) + { + __instance.YouAreText.color = Utils.GetRoleColor(role); + __instance.RoleText.text = Utils.GetRoleName(role); + __instance.RoleText.color = Utils.GetRoleColor(role); + __instance.RoleBlurbText.color = Utils.GetRoleColor(role); + __instance.RoleBlurbText.text = localPlayer.GetRoleInfo(); + } - foreach (var subRole in Main.PlayerStates[localPlayer.PlayerId].SubRoles.ToArray()) - __instance.RoleBlurbText.text += "\n" + Utils.ColorString(Utils.GetRoleColor(subRole), GetString($"{subRole}Info")); + foreach (var subRole in Main.PlayerStates[localPlayer.PlayerId].SubRoles.ToArray()) + __instance.RoleBlurbText.text += "\n" + Utils.ColorString(Utils.GetRoleColor(subRole), GetString($"{subRole}Info")); - __instance.RoleText.text += Utils.GetSubRolesText(localPlayer.PlayerId, false, true); + __instance.RoleText.text += Utils.GetSubRolesText(localPlayer.PlayerId, false, true); + break; } }, 0.0001f, "Override Role Text"); @@ -404,171 +413,198 @@ public static void Postfix(IntroCutscene __instance) __instance.ImpostorText.gameObject.SetActive(false); - switch (role.GetCustomRoleTeam()) + if (role is CustomRoles.GM) + { + __instance.TeamTitle.text = Utils.GetRoleName(role); + __instance.TeamTitle.color = Utils.GetRoleColor(role); + __instance.BackgroundBar.material.color = Utils.GetRoleColor(role); + __instance.ImpostorText.gameObject.SetActive(false); + PlayerControl.LocalPlayer.Data.Role.IntroSound = DestroyableSingleton.Instance.TaskCompleteSound; + } + switch (Options.CurrentGameMode) { - case Custom_Team.Impostor: - __instance.TeamTitle.text = GetString("TeamImpostor"); - __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(255, 25, 25, byte.MaxValue); - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Impostor); + case CustomGameMode.FFA: + __instance.TeamTitle.text = GetString("FFA"); ; + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(0, 255, 255, byte.MaxValue); + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Shapeshifter); __instance.ImpostorText.gameObject.SetActive(true); - __instance.ImpostorText.text = GetString("SubText.Impostor"); + __instance.ImpostorText.text = GetString("KillerInfo"); break; - case Custom_Team.Crewmate: - __instance.TeamTitle.text = GetString("TeamCrewmate"); - __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(140, 255, 255, byte.MaxValue); - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Crewmate); + case CustomGameMode.CandR: //C&R + __instance.TeamTitle.text = $"{GetString("C&R")}"; __instance.ImpostorText.gameObject.SetActive(true); - __instance.ImpostorText.text = GetString("SubText.Crewmate"); + __instance.ImpostorText.text = GetString("C&RShortInfo"); + __instance.TeamTitle.color = Color.blue; + __instance.BackgroundBar.material.color = Color.blue; + StartFadeIntro(__instance, Color.blue, Color.red, changeInterval: 400); + switch (role) + { + case CustomRoles.Cop: + PlayerControl.LocalPlayer.Data.Role.IntroSound = ShipStatus.Instance.SabotageSound; + break; + case CustomRoles.Robber: + PlayerControl.LocalPlayer.Data.Role.IntroSound = DestroyableSingleton.Instance.TaskCompleteSound; + break; + } break; - case Custom_Team.Neutral: - if (!role.IsNA()) + default: + switch (role.GetCustomRoleTeam()) + { + case Custom_Team.Impostor: + __instance.TeamTitle.text = GetString("TeamImpostor"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(255, 25, 25, byte.MaxValue); + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Impostor); + __instance.ImpostorText.gameObject.SetActive(true); + __instance.ImpostorText.text = GetString("SubText.Impostor"); + break; + case Custom_Team.Crewmate: + __instance.TeamTitle.text = GetString("TeamCrewmate"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(140, 255, 255, byte.MaxValue); + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Crewmate); + __instance.ImpostorText.gameObject.SetActive(true); + __instance.ImpostorText.text = GetString("SubText.Crewmate"); + break; + case Custom_Team.Neutral: + if (!role.IsNA()) + { + __instance.TeamTitle.text = GetString("TeamNeutral"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(127, 140, 141, byte.MaxValue); + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Shapeshifter); + __instance.ImpostorText.gameObject.SetActive(true); + __instance.ImpostorText.text = GetString("SubText.Neutral"); + } + else + { + __instance.TeamTitle.text = GetString("TeamApocalypse"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(255, 23, 79, byte.MaxValue); + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Phantom); + __instance.ImpostorText.gameObject.SetActive(true); + __instance.ImpostorText.text = GetString("SubText.Apocalypse"); + } + break; + } + + switch (role) { - __instance.TeamTitle.text = GetString("TeamNeutral"); - __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(127, 140, 141, byte.MaxValue); + case CustomRoles.ShapeMaster: + case CustomRoles.ShapeshifterTOHE: + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Shapeshifter); + break; + case CustomRoles.CursedSoul: + case CustomRoles.SoulCatcher: + case CustomRoles.Specter: + case CustomRoles.Stalker: + case CustomRoles.PhantomTOHE: + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Phantom); + break; + case CustomRoles.Coroner: + case CustomRoles.TrackerTOHE: + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Tracker); + break; + case CustomRoles.Celebrity: + case CustomRoles.NoisemakerTOHE: + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Noisemaker); + break; + case CustomRoles.EngineerTOHE: + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Engineer); + break; + case CustomRoles.Doctor: + case CustomRoles.Medic: + case CustomRoles.ScientistTOHE: + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Scientist); + break; + case CustomRoles.Observer: + case CustomRoles.Spiritualist: + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.GuardianAngel); + break; + + case CustomRoles.Terrorist: + case CustomRoles.Bomber: + var sound = ShipStatus.Instance.CommonTasks.FirstOrDefault(task => task.TaskType == TaskTypes.FixWiring) + .MinigamePrefab.OpenSound; + PlayerControl.LocalPlayer.Data.Role.IntroSound = sound; + break; + + case CustomRoles.Workaholic: + case CustomRoles.Snitch: + case CustomRoles.TaskManager: + PlayerControl.LocalPlayer.Data.Role.IntroSound = DestroyableSingleton.Instance.TaskCompleteSound; + break; + + case CustomRoles.Opportunist: + case CustomRoles.Hater: + case CustomRoles.Revolutionist: + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Crewmate); + break; + + case CustomRoles.Addict: + case CustomRoles.Ventguard: + PlayerControl.LocalPlayer.Data.Role.IntroSound = ShipStatus.Instance.VentEnterSound; + break; + + case CustomRoles.Saboteur: + case CustomRoles.Inhibitor: + case CustomRoles.Mechanic: + case CustomRoles.Provocateur: + PlayerControl.LocalPlayer.Data.Role.IntroSound = ShipStatus.Instance.SabotageSound; + break; + + case CustomRoles.Pixie: + case CustomRoles.Seeker: + PlayerControl.LocalPlayer.Data.Role.IntroSound = DestroyableSingleton.Instance.HnSOtherImpostorTransformSfx; + break; + + case CustomRoles.GM: + __instance.TeamTitle.text = Utils.GetRoleName(role); + __instance.TeamTitle.color = Utils.GetRoleColor(role); + __instance.BackgroundBar.material.color = Utils.GetRoleColor(role); + __instance.ImpostorText.gameObject.SetActive(true); + PlayerControl.LocalPlayer.Data.Role.IntroSound = DestroyableSingleton.Instance.TaskCompleteSound; + __instance.ImpostorText.text = GetString("SubText.GM"); + break; + + case CustomRoles.ChiefOfPolice: + case CustomRoles.Sheriff: + case CustomRoles.Veteran: + case CustomRoles.Knight: + case CustomRoles.KillingMachine: + case CustomRoles.Reverie: + case CustomRoles.NiceGuesser: + case CustomRoles.Vigilante: + PlayerControl.LocalPlayer.Data.Role.IntroSound = PlayerControl.LocalPlayer.KillSfx; + break; + case CustomRoles.Swooper: + case CustomRoles.Wraith: + case CustomRoles.Chameleon: + PlayerControl.LocalPlayer.Data.Role.IntroSound = PlayerControl.LocalPlayer.MyPhysics.ImpostorDiscoveredSound; + break; + } + if (PlayerControl.LocalPlayer.Is(CustomRoles.Lovers)) + { + __instance.TeamTitle.text = GetString("TeamLovers"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(255, 154, 206, byte.MaxValue); + __instance.ImpostorText.gameObject.SetActive(true); + __instance.ImpostorText.text = GetString("SubText.Lovers"); + } + else if (PlayerControl.LocalPlayer.Is(CustomRoles.Egoist)) + { + __instance.TeamTitle.text = GetString("TeamEgoist"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(86, 0, 255, byte.MaxValue); PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Shapeshifter); __instance.ImpostorText.gameObject.SetActive(true); - __instance.ImpostorText.text = GetString("SubText.Neutral"); + __instance.ImpostorText.text = GetString("SubText.Egoist"); } - else + else if (PlayerControl.LocalPlayer.Is(CustomRoles.Madmate) || role.IsMadmate()) { - __instance.TeamTitle.text = GetString("TeamApocalypse"); - __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(255, 23, 79, byte.MaxValue); - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Phantom); + __instance.TeamTitle.text = GetString("TeamMadmate"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(255, 25, 25, byte.MaxValue); + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Impostor); __instance.ImpostorText.gameObject.SetActive(true); - __instance.ImpostorText.text = GetString("SubText.Apocalypse"); + __instance.ImpostorText.text = GetString("SubText.Madmate"); } break; } - switch (role) - { - case CustomRoles.ShapeMaster: - case CustomRoles.ShapeshifterTOHE: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Shapeshifter); - break; - case CustomRoles.CursedSoul: - case CustomRoles.SoulCatcher: - case CustomRoles.Specter: - case CustomRoles.Stalker: - case CustomRoles.PhantomTOHE: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Phantom); - break; - case CustomRoles.Coroner: - case CustomRoles.TrackerTOHE: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Tracker); - break; - case CustomRoles.Celebrity: - case CustomRoles.NoisemakerTOHE: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Noisemaker); - break; - case CustomRoles.EngineerTOHE: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Engineer); - break; - case CustomRoles.Doctor: - case CustomRoles.Medic: - case CustomRoles.ScientistTOHE: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Scientist); - break; - case CustomRoles.Observer: - case CustomRoles.Spiritualist: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.GuardianAngel); - break; - - case CustomRoles.Terrorist: - case CustomRoles.Bomber: - var sound = ShipStatus.Instance.CommonTasks.FirstOrDefault(task => task.TaskType == TaskTypes.FixWiring) - .MinigamePrefab.OpenSound; - PlayerControl.LocalPlayer.Data.Role.IntroSound = sound; - break; - - case CustomRoles.Workaholic: - case CustomRoles.Snitch: - case CustomRoles.TaskManager: - PlayerControl.LocalPlayer.Data.Role.IntroSound = DestroyableSingleton.Instance.TaskCompleteSound; - break; - - case CustomRoles.Opportunist: - case CustomRoles.Hater: - case CustomRoles.Revolutionist: - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Crewmate); - break; - - case CustomRoles.Addict: - case CustomRoles.Ventguard: - PlayerControl.LocalPlayer.Data.Role.IntroSound = ShipStatus.Instance.VentEnterSound; - break; - - case CustomRoles.Saboteur: - case CustomRoles.Inhibitor: - case CustomRoles.Mechanic: - case CustomRoles.Provocateur: - PlayerControl.LocalPlayer.Data.Role.IntroSound = ShipStatus.Instance.SabotageSound; - break; - - case CustomRoles.Pixie: - case CustomRoles.Seeker: - PlayerControl.LocalPlayer.Data.Role.IntroSound = DestroyableSingleton.Instance.HnSOtherImpostorTransformSfx; - break; - - case CustomRoles.GM: - __instance.TeamTitle.text = Utils.GetRoleName(role); - __instance.TeamTitle.color = Utils.GetRoleColor(role); - __instance.BackgroundBar.material.color = Utils.GetRoleColor(role); - __instance.ImpostorText.gameObject.SetActive(true); - PlayerControl.LocalPlayer.Data.Role.IntroSound = DestroyableSingleton.Instance.TaskCompleteSound; - __instance.ImpostorText.text = GetString("SubText.GM"); - break; - - case CustomRoles.ChiefOfPolice: - case CustomRoles.Sheriff: - case CustomRoles.Veteran: - case CustomRoles.Knight: - case CustomRoles.KillingMachine: - case CustomRoles.Reverie: - case CustomRoles.NiceGuesser: - case CustomRoles.Vigilante: - PlayerControl.LocalPlayer.Data.Role.IntroSound = PlayerControl.LocalPlayer.KillSfx; - break; - case CustomRoles.Swooper: - case CustomRoles.Wraith: - case CustomRoles.Chameleon: - PlayerControl.LocalPlayer.Data.Role.IntroSound = PlayerControl.LocalPlayer.MyPhysics.ImpostorDiscoveredSound; - break; - } - - if (PlayerControl.LocalPlayer.Is(CustomRoles.Lovers)) - { - __instance.TeamTitle.text = GetString("TeamLovers"); - __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(255, 154, 206, byte.MaxValue); - __instance.ImpostorText.gameObject.SetActive(true); - __instance.ImpostorText.text = GetString("SubText.Lovers"); - } - else if (PlayerControl.LocalPlayer.Is(CustomRoles.Egoist)) - { - __instance.TeamTitle.text = GetString("TeamEgoist"); - __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(86, 0, 255, byte.MaxValue); - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Shapeshifter); - __instance.ImpostorText.gameObject.SetActive(true); - __instance.ImpostorText.text = GetString("SubText.Egoist"); - } - else if (PlayerControl.LocalPlayer.Is(CustomRoles.Madmate) || role.IsMadmate()) - { - __instance.TeamTitle.text = GetString("TeamMadmate"); - __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(255, 25, 25, byte.MaxValue); - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Impostor); - __instance.ImpostorText.gameObject.SetActive(true); - __instance.ImpostorText.text = GetString("SubText.Madmate"); - } - - if (Options.CurrentGameMode == CustomGameMode.FFA) - { - __instance.TeamTitle.text = "FREE FOR ALL"; - __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(0, 255, 255, byte.MaxValue); - PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Shapeshifter); - __instance.ImpostorText.gameObject.SetActive(true); - __instance.ImpostorText.text = "KILL EVERYONE TO WIN"; - } - // I hope no one notices this in code // unfortunately niko noticed while fixing others' shxt if (Input.GetKey(KeyCode.RightShift)) @@ -592,14 +628,14 @@ public static AudioClip GetIntroSound(RoleTypes roleType) { return RoleManager.Instance.AllRoles.FirstOrDefault((role) => role.Role == roleType)?.IntroSound; } - private static async void StartFadeIntro(IntroCutscene __instance, Color start, Color end) + private static async void StartFadeIntro(IntroCutscene __instance, Color start, Color end, int changeInterval = 20) { await Task.Delay(1000); int milliseconds = 0; while (true) { - await Task.Delay(20); - milliseconds += 20; + await Task.Delay(changeInterval); + milliseconds += changeInterval; float time = milliseconds / (float)500; Color LerpingColor = Color.Lerp(start, end, time); if (__instance == null || milliseconds > 500) @@ -777,7 +813,8 @@ public static void Prefix() AmongUsClient.Instance.SendOrDisconnect(writer); writer.Recycle(); - UnShapeshifter.ResetPlayerOutfit(force: true); + if (Options.CurrentGameMode is CustomGameMode.CandR) CopsAndRobbersManager.SetCostume(CopsAndRobbersManager.RoleType.Cop, UnShapeshifter.PlayerId); + else UnShapeshifter.ResetPlayerOutfit(force: true); } else { @@ -865,11 +902,25 @@ public static void Postfix() bool chatVisible = Options.CurrentGameMode switch { CustomGameMode.FFA => true, + CustomGameMode.CandR => CopsAndRobbersManager.CandR_ShowChatInGame.GetBool(), + _ => false + }; + bool shouldAntiBlackOut = Options.CurrentGameMode switch + { + CustomGameMode.CandR => CopsAndRobbersManager.CandR_ShowChatInGame.GetBool(), _ => false }; try { if (chatVisible) Utils.SetChatVisibleForEveryone(); + if (shouldAntiBlackOut) + { + _ = new LateTask(() => + { + AntiBlackout.SetIsDead(); + Logger.Warn("Set is dead", "IntroPatch"); + }, 5f, "anti blackout"); + } } catch (Exception error) { diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 4bb89ae64..b2055506e 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -203,6 +203,12 @@ public static bool CheckForInvalidMurdering(PlayerControl killer, PlayerControl FFAManager.OnPlayerAttack(killer, target); return false; } + //C&R + if (Options.CurrentGameMode == CustomGameMode.CandR) + { + CopsAndRobbersManager.OnCopAttack(killer, target); + return false; + } // if player hacked by Glitch if (Glitch.HasEnabled && !Glitch.OnCheckMurderOthers(killer, target)) @@ -568,7 +574,7 @@ public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerC bool resetCooldown = true; logger.Info($"Self:{shapeshifter.PlayerId == target.PlayerId} - Is animate:{shouldAnimate} - In Meeting:{GameStates.IsMeeting}"); - + var shapeshifterRoleClass = shapeshifter.GetRoleClass(); if (shapeshifterRoleClass?.OnCheckShapeshift(shapeshifter, target, ref resetCooldown, ref shouldAnimate) == false) { @@ -630,25 +636,38 @@ private static bool CheckInvalidShapeshifting(PlayerControl instance, PlayerCont Logger.Info("Checking while AntiBlackOut protect, shapeshift was canceled", "CheckShapeshift"); return false; } - if (!(instance.Is(CustomRoles.ShapeshifterTOHE) || instance.Is(CustomRoles.Shapeshifter)) && target.GetClient().GetHashedPuid() == Main.FirstDiedPrevious && MeetingStates.FirstMeeting) + if (Options.CurrentGameMode is CustomGameMode.CandR) { - instance.RpcGuardAndKill(instance); - instance.Notify(Utils.ColorString(Utils.GetRoleColor(instance.GetCustomRole()), GetString("PlayerIsShieldedByGame"))); - logger.Info($"Cancel shapeshifting because {target.GetRealName()} is protected by the game"); - return false; + if (instance == target && Main.UnShapeShifter.Contains(instance.PlayerId)) + { + if (!instance.IsMushroomMixupActive() && !GameStates.IsMeeting) CopsAndRobbersManager.UnShapeShiftButton(instance); + instance.RpcResetAbilityCooldown(); // Just incase + logger.Info($"Cancel shapeshifting because {instance.GetRealName()} is using un-shapeshift ability button"); + return false; + } } - if (Pelican.IsEaten(instance.PlayerId)) + else { - logger.Info($"Cancel shapeshifting because {instance.GetRealName()} is eaten by Pelican"); - return false; - } + if (!(instance.Is(CustomRoles.ShapeshifterTOHE) || instance.Is(CustomRoles.Shapeshifter)) && target.GetClient().GetHashedPuid() == Main.FirstDiedPrevious && MeetingStates.FirstMeeting) + { + instance.RpcGuardAndKill(instance); + instance.Notify(Utils.ColorString(Utils.GetRoleColor(instance.GetCustomRole()), GetString("PlayerIsShieldedByGame"))); + logger.Info($"Cancel shapeshifting because {target.GetRealName()} is protected by the game"); + return false; + } + if (Pelican.IsEaten(instance.PlayerId)) + { + logger.Info($"Cancel shapeshifting because {instance.GetRealName()} is eaten by Pelican"); + return false; + } - if (instance == target && Main.UnShapeShifter.Contains(instance.PlayerId)) - { - if (!instance.IsMushroomMixupActive() && !GameStates.IsMeeting) instance.GetRoleClass().UnShapeShiftButton(instance); - instance.RpcResetAbilityCooldown(); // Just incase - logger.Info($"Cancel shapeshifting because {instance.GetRealName()} is using un-shapeshift ability button"); - return false; + if (instance == target && Main.UnShapeShifter.Contains(instance.PlayerId)) + { + if (!instance.IsMushroomMixupActive() && !GameStates.IsMeeting) instance.GetRoleClass().UnShapeShiftButton(instance); + instance.RpcResetAbilityCooldown(); // Just incase + logger.Info($"Cancel shapeshifting because {instance.GetRealName()} is using un-shapeshift ability button"); + return false; + } } return true; } @@ -723,7 +742,7 @@ public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] Network return false; } if (Options.DisableMeeting.GetBool()) return false; - if (Options.CurrentGameMode == CustomGameMode.FFA) return false; + if (Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.CandR) return false; if (!CanReport[__instance.PlayerId]) { @@ -1389,9 +1408,15 @@ public static Task DoPostfix(PlayerControl __instance) } - if (Options.CurrentGameMode == CustomGameMode.FFA) - Suffix.Append(FFAManager.GetPlayerArrow(seer, target)); - + switch (Options.CurrentGameMode) + { + case CustomGameMode.FFA: + Suffix.Append(FFAManager.GetPlayerArrow(seer, target)); + break; + case CustomGameMode.CandR: + Suffix.Append(CopsAndRobbersManager.GetClosestArrow(seer, target)); + break; + } /*if(main.AmDebugger.Value && main.BlockKilling.TryGetValue(target.PlayerId, out var isBlocked)) { Mark = isBlocked ? "(true)" : "(false)";}*/ @@ -1623,6 +1648,10 @@ public static void Postfix(PlayerPhysics __instance, [HarmonyArgument(0)] int id } if (!AmongUsClient.Instance.AmHost) return; + if (Options.CurrentGameMode == CustomGameMode.CandR) + { + CopsAndRobbersManager.OnRobberExitVent(player); + } player.GetRoleClass()?.OnExitVent(player, id); if (player.GetRoleClass()?.BlockMoveInVent(player) ?? true) diff --git a/Patches/PlayerJoinAndLeftPatch.cs b/Patches/PlayerJoinAndLeftPatch.cs index 885514918..f71b5b711 100644 --- a/Patches/PlayerJoinAndLeftPatch.cs +++ b/Patches/PlayerJoinAndLeftPatch.cs @@ -366,6 +366,10 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)] Client { if (GameStates.IsNormalGame && GameStates.IsInGame) { + if (Options.CurrentGameMode is CustomGameMode.CandR) + { + CopsAndRobbersManager.OnPlayerDisconnect(data.Character.PlayerId); + } if (data.Character.Is(CustomRoles.Lovers) && !data.Character.Data.IsDead) { foreach (var lovers in Main.LoversPlayers.ToArray()) diff --git a/Patches/onGameStartedPatch.cs b/Patches/onGameStartedPatch.cs index 80b4fc7ca..9df3083eb 100644 --- a/Patches/onGameStartedPatch.cs +++ b/Patches/onGameStartedPatch.cs @@ -221,6 +221,9 @@ public static void Postfix(AmongUsClient __instance) //FFA FFAManager.Init(); + //C&R + CopsAndRobbersManager.Init(); + FallFromLadder.Reset(); CustomWinnerHolder.Reset(); @@ -419,13 +422,21 @@ public static System.Collections.IEnumerator AssignRoles() Main.PlayerStates[pc.PlayerId].SetMainRole(role); } - if (Options.CurrentGameMode == CustomGameMode.FFA) + switch (Options.CurrentGameMode) { - foreach (var pair in Main.PlayerStates) - { - ExtendedPlayerControl.RpcSetCustomRole(pair.Key, pair.Value.MainRole); - } - goto EndOfSelectRolePatch; + case CustomGameMode.FFA: + foreach (var pair in Main.PlayerStates) + { + ExtendedPlayerControl.RpcSetCustomRole(pair.Key, pair.Value.MainRole); + } + goto EndOfSelectRolePatch; + case CustomGameMode.CandR: + foreach (var pair in RoleAssign.RoleResult) + { + if (pair.Value is CustomRoles.Robber) AssignCustomRole(pair.Value, Utils.GetPlayerById(pair.Key)); + ExtendedPlayerControl.RpcSetCustomRole(pair.Key, pair.Value); + } + goto EndOfSelectRolePatch; } foreach (var kv in RoleAssign.RoleResult) @@ -502,6 +513,9 @@ public static System.Collections.IEnumerator AssignRoles() case CustomGameMode.FFA: GameEndCheckerForNormal.SetPredicateToFFA(); break; + case CustomGameMode.CandR: + GameEndCheckerForNormal.SetPredicateToCandR(); + break; } EAC.LogAllRoles(); diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 42af7dc5c..4e9b6d1d4 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -3705,6 +3705,79 @@ "LongMode": "Enable to have a long neck", "InfluencedChangeVote": "Oops! You are so influenced by others!\nYou can not contain your fear that you change voted {0}!", + "C&R": "Cops and Robbers", + "CandR": "Cops and Robbers", + "C&RShortInfo": "Robbers complete tasks, while Cops race to capture them!", + "ModeDescribe.C&R": "Welcome to the ultimate heist showdown! In this thrilling Among Us mode, players are split into two teams: Cops and Robbers.\n\nAs a Robber, your mission is to stealthily complete all tasks and escape the vigilant eyes of the Cops. If you come across a captured player, simply touch them to set them free! Utilize vents to activate random abilities that can help you outsmart your pursuers. Teamwork and cunning are key—can you pull off the perfect heist?\n\nAs a Cop, your job is to track down and capture the robbers before they can finish their tasks. Use your kill button to freeze them in place, and trigger powerful random abilities with your shapeshift button to turn the tide in your favor. It’s a game of cat and mouse—can you outsmart the robbers and secure a win for the team?\n\nJoin the chase, strategize with your crew, and may the best team emerge victorious!", + "ModeC&R": "Gamemode: Cops And Robbers", + "Cop": "Cop", + "CopInfo": "Capture all Robbers to win", + "CopInfoLong": "As a cop, use your kill button to capture robbers before they finish their tasks. When caught, robbers freeze in place.\n\nYou can trigger a random ability with the shapeshift button.\nUse /ability cop to check all the abilities.\n\nTrack down the robbers and secure your win!", + "WinnerRoleText.Cop": "Cops Win!", + "C&R_NumCops": "Number of Cops", + "C&R_CopAbilityTriggerChance": "Cop ability trigger chance", + "C&R_CaptureCooldown": "Capture Cooldown", + "C&R_TeleportCaptureToRandomLoc": "Teleport captured robber to random location", + "C&R_CopAbilityDuration": "Cop ability duration", + "C&R_CopAbilityCooldown": "Cop ability cooldown", + "C&R_HotPursuitChance": "Hot Pursuit activation chance", + "C&R_HotPursuitSpeed": "Increase speed by", + "C&R_SpikeStripChance": "Spike Strip activation chance", + "C&R_SpikeStripRadius": "Spike Strip trigger radius", + "C&R_SpikeStripDuration": "Spike Strip duration", + "C&R_FlashBangChance": "Flash Bang activation chance", + "C&R_FlashBangRadius": "Flash Bang radius", + "C&R_FlashBangDuration": "Flash Bang duration", + "C&R_K9Chance": "K9 chance", + "C&R_ScopeChance": "Scope chance", + "C&R_ScopeIncrease": "Scope Increase", + "C&R_AbilityActivated": "{Ability.Name} activated", + "CopAbility.HotPursuit": "Hot Pursuit", + "CopAbility.SpikeStrip": "Spike Strip", + "CopAbility.FlashBang": "Flash Bang", + "CopAbility.K9": "K9", + "CopAbility.Scope": "Scope", + "Description.HotPursuit": "Increase speed of the Cop.", + "Description.SpikeStrip": "Cop sets a trap, when triggered freezes the robber.", + "Description.FlashBang": "Cop Sets a trap, when triggered blinds the robber.", + "Description.K9": "Cop gets an arrow pointing to the closest robber.", + "Description.Scope": "Increases the capture range of cop.", + "CopAbilityText": "Ability", + "CopKillButtonText": "Capture", + "C&R_NotifyRobbersWhenCaptured": "Notify Robbers when captured", + "Robber": "Robber", + "RobberInfo": "Complete all tasks to win", + "RobberInfoLong": "As robber, your goal is to complete all tasks and escape the watchful eyes of the cops! If you encounter a captured player, you can touch them to set them free!\n\nWhen you emerge from a vent, you’ll activate a random ability.\nUse /ability robber to check all the abilities. Work together, stay stealthy, and outsmart the cops to pull of a great Heist!", + "WinnerRoleText.Robber": "Robber Win!", + "C&R_RobberVentDuration": "Robber vent duration", + "C&R_RobberVentCooldown": "Robber vent cooldown", + "C&R_RobberAbilityTriggerChance": "Robber ability trigger chance", + "C&R_RobberAbilityDuration": "Robber ability duration", + "C&R_AdrenalineRushChance": "Adrenaline Rush activation chance", + "C&R_AdrenalineRushSpeed": "Increase speed by", + "C&R_EnergyShieldChance": "Energy Shield activation chance", + "C&R_SmokeBombChance": "Smoke Bomb activation chance", + "C&R_SmokeBombDuration": "Smoke Bomb duration", + "C&R_DisguiseChance": "Disguise activation chance", + "C&R_RadarChance": "Radar activation chance", + "RobberAbility.AdrenalineRush": "Adrenaline Rush", + "RobberAbility.EnergyShield": "Energy Shield", + "RobberAbility.SmokeBomb": "Smoke Bomb", + "RobberAbility.Disguise": "Disguise", + "RobberAbility.Radar": "Radar", + "Description.AdrenalineRush": "Increases the speed of Robber.", + "Description.EnergyShield": "Protects from the capture attempt.", + "Description.SmokeBomb": "Blinds the Cop when robber is captured.", + "Description.Disguise": "Wear the same costume as Cop.", + "Description.Radar": "Points towards closest captured player, if no captured player, it points towards a cop (colored arrows indicating cops or captured).", + "Captured": "Captured", + "Saved": "Saved", + "C&R_CapturedInReleaseCooldown": "Release cooldown for captured player is not finished", + "C&R_RobberInReleaseCooldown": "Your release cooldown is not finished", + "C&R_ReleaseCooldownForCaptured": "Release cooldown for captured player", + "C&R_ReleaseCooldownForRobber": "Release cooldown for robber", + "CandR_GameTime": "Maximum Game Length", + "C&R_ShowChatInGame": "Show chat in game (may cause black screens)", "FFA": "Free For All", "ModeFFA": "Gamemode: FFA", diff --git a/Resources/roleColor.json b/Resources/roleColor.json index f91ab992e..c02884186 100644 --- a/Resources/roleColor.json +++ b/Resources/roleColor.json @@ -249,5 +249,7 @@ "Sloth": "#376db8", "Eavesdropper": "#ffe6bf", "Shocker": "#CCCC00", - "Revenant": "#cc9329" + "Revenant": "#cc9329", + "Cop": "#007BFF", + "Robber": "#FF8C00" } diff --git a/Roles/Core/AssignManager/AddonAssign.cs b/Roles/Core/AssignManager/AddonAssign.cs index 1d58f36b2..845463e45 100644 --- a/Roles/Core/AssignManager/AddonAssign.cs +++ b/Roles/Core/AssignManager/AddonAssign.cs @@ -31,8 +31,12 @@ private static bool NotAssignAddOnInGameStarted(CustomRoles role) public static void StartSelect() { - if (Options.CurrentGameMode == CustomGameMode.FFA) return; - + switch (Options.CurrentGameMode) + { + case CustomGameMode.FFA: + case CustomGameMode.CandR: + return; + } AddonRolesList.Clear(); foreach (var cr in CustomRolesHelper.AllRoles) { diff --git a/Roles/Core/AssignManager/RoleAssign.cs b/Roles/Core/AssignManager/RoleAssign.cs index c4f7950ad..6e205e7cd 100644 --- a/Roles/Core/AssignManager/RoleAssign.cs +++ b/Roles/Core/AssignManager/RoleAssign.cs @@ -59,6 +59,10 @@ public static void StartSelect() RoleResult[pc.PlayerId] = CustomRoles.Killer; } return; + case CustomGameMode.CandR: + RoleResult = []; + RoleResult = CopsAndRobbersManager.SetRoles(); + return; } var rd = IRandom.Instance; diff --git a/main.cs b/main.cs index 78e73cb09..a9d26846a 100644 --- a/main.cs +++ b/main.cs @@ -910,6 +910,10 @@ public enum CustomRoles //FFA Killer, + //C&R + Cop, + Robber, + //GM GM, @@ -1064,6 +1068,8 @@ public enum CustomWinner Solsticer = CustomRoles.Solsticer, Shocker = CustomRoles.Shocker, Apocalypse = CustomRoles.Apocalypse, + Robbers = CustomRoles.Robber, //C&R + Cops = CustomRoles.Cop, //C&R } [Obfuscation(Exclude = true)] public enum AdditionalWinners