diff --git a/CustomRoles.cs b/CustomRoles.cs index 9e0fa6425..bcde248ba 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -198,7 +198,7 @@ public enum CustomRoles Lighter, Lookout, Luckey, - Lyncher, + Decryptor, Markseeker, Marshall, Mathematician, diff --git a/GameMode/NaturalDisasters.cs b/GameMode/NaturalDisasters.cs index 3e8f87a1d..a33bc73a5 100644 --- a/GameMode/NaturalDisasters.cs +++ b/GameMode/NaturalDisasters.cs @@ -263,7 +263,7 @@ public static class NaturalDisasters { "BuildingCollapse" => roomKvp.Value, "Thunderstorm" => Pelican.GetBlackRoomPS(), - _ => IRandom.Instance.Next(2) == 0 + _ => IRandom.Instance.Next(2) == 0 && Options.CurrentGameMode == CustomGameMode.NaturalDisasters ? Main.AllAlivePlayerControls.RandomElement().Pos() : new(Random.Range(MapBounds.X.Left, MapBounds.X.Right), Random.Range(MapBounds.Y.Top, MapBounds.Y.Bottom)) }; diff --git a/GameMode/RoomRush.cs b/GameMode/RoomRush.cs index 066b26a7d..8ab5984a1 100644 --- a/GameMode/RoomRush.cs +++ b/GameMode/RoomRush.cs @@ -223,7 +223,7 @@ public static class RoomRush Map = RandomSpawn.SpawnMap.GetSpawnMap(); - yield return new WaitForSeconds(Main.CurrentMap == MapNames.Airship ? 4f : 0.2f); + yield return new WaitForSeconds(Main.CurrentMap == MapNames.Airship ? 8f : 4f); PlayerControl[] aapc = Main.AllAlivePlayerControls; aapc.Do(x => x.RpcSetCustomRole(CustomRoles.RRPlayer)); @@ -234,7 +234,7 @@ public static class RoomRush if (showTutorial) { - var readingTime = 2; + var readingTime = 0; StringBuilder sb = new(Translator.GetString("RR_Tutorial_Basics")); sb.AppendLine(); @@ -464,6 +464,7 @@ public static class RoomRush switch (reader.ReadPackedInt32()) { case 1: + PointsToWinValue = PointsToWin.GetInt() * Main.AllAlivePlayerControls.Length; int ventLimit = VentTimes.GetInt(); VentLimit = Main.AllPlayerControls.ToDictionary(x => x.PlayerId, _ => ventLimit); if (WinByPointsInsteadOfDeaths.GetBool()) Points = Main.AllPlayerControls.ToDictionary(x => x.PlayerId, _ => 0); @@ -603,7 +604,8 @@ public class RRPlayer : RoleBase public override void OnExitVent(PlayerControl pc, Vent vent) { RoomRush.VentLimit[pc.PlayerId]--; - Utils.SendRPC(CustomRPC.RoomRushDataSync, 2, RoomRush.VentLimit[pc.PlayerId], pc.PlayerId); - if (RoomRush.VentLimit[pc.PlayerId] <= 0) pc.RpcChangeRoleBasis(CustomRoles.RRPlayer); + int newLimit = RoomRush.VentLimit[pc.PlayerId]; + Utils.SendRPC(CustomRPC.RoomRushDataSync, 2, newLimit, pc.PlayerId); + if (newLimit <= 0) pc.RpcChangeRoleBasis(CustomRoles.RRPlayer); } } \ No newline at end of file diff --git a/Main.cs b/Main.cs index d4cc66794..d65c51112 100644 --- a/Main.cs +++ b/Main.cs @@ -402,7 +402,7 @@ public class Main : BasePlugin { CustomRoles.Oxyman, "#ffa58c" }, { CustomRoles.Rhapsode, "#f5ad42" }, { CustomRoles.Chef, "#d6d6ff" }, - { CustomRoles.Lyncher, "#14ba7d" }, + { CustomRoles.Decryptor, "#14ba7d" }, { CustomRoles.Socialite, "#32a8a8" }, { CustomRoles.Adrenaline, "#ffff00" }, { CustomRoles.Safeguard, "#4949e3" }, diff --git a/Modules/Achievements.cs b/Modules/Achievements.cs index c0604db97..9915ebef9 100644 --- a/Modules/Achievements.cs +++ b/Modules/Achievements.cs @@ -76,7 +76,7 @@ public static class Achievements Speedrun, // Have a game end in under 1 minute Carried, // Win as a madmate while the other impostors are dead SorryToBurstYourBubble, // Explode 5 people with 1 encased player as the Bubble - GetLynched, // Successfully guess 3 roles as lyncher + GetDecrypted, // Successfully guess 3 roles as Decryptor FlagMaster, // Carry the flag the longest in CTF Tag, // Tag the most players in CTF Vectory, // Vent 50 times in one game @@ -230,6 +230,7 @@ public static class Achievements IEnumerator FetchAchievementsFromApiAsync() { while (PlayerControl.LocalPlayer == null) yield return null; + yield return new WaitForSeconds(3f); string userId = PlayerControl.LocalPlayer.GetClient().GetHashedPuid(); var url = $"{ApiLoadEndpoint}?userId={userId}"; diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index c2843fc59..37299201e 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -237,7 +237,7 @@ internal static class CustomRolesHelper CustomRoles.President => CustomRoles.Crewmate, CustomRoles.Oxyman => CustomRoles.Engineer, CustomRoles.Chef => CustomRoles.Crewmate, - CustomRoles.Lyncher => CustomRoles.Crewmate, + CustomRoles.Decryptor => CustomRoles.Crewmate, CustomRoles.Adrenaline => CustomRoles.Crewmate, CustomRoles.Safeguard => CustomRoles.Crewmate, CustomRoles.Clairvoyant => UsePets ? CustomRoles.Crewmate : CustomRoles.Engineer, @@ -907,7 +907,7 @@ internal static class CustomRolesHelper CustomRoles.Merchant or CustomRoles.Mayor or CustomRoles.Insight or - CustomRoles.Lyncher or + CustomRoles.Decryptor or CustomRoles.Transporter; } @@ -1730,7 +1730,7 @@ internal static class CustomRolesHelper CustomRoles.Detour => RoleOptionType.Crewmate_Power, CustomRoles.Dictator => RoleOptionType.Crewmate_Power, CustomRoles.Guardian => RoleOptionType.Crewmate_Power, - CustomRoles.Lyncher => RoleOptionType.Crewmate_Power, + CustomRoles.Decryptor => RoleOptionType.Crewmate_Power, CustomRoles.Marshall => RoleOptionType.Crewmate_Power, CustomRoles.Mayor => RoleOptionType.Crewmate_Power, CustomRoles.NiceEraser => RoleOptionType.Crewmate_Power, diff --git a/Modules/CustomSabotage.cs b/Modules/CustomSabotage.cs new file mode 100644 index 000000000..d9d7d3b95 --- /dev/null +++ b/Modules/CustomSabotage.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; +using UnityEngine; + +namespace EHR.Modules; + +public abstract class CustomSabotage +{ + public static readonly List Instances = []; + + public virtual void Deteriorate() + { + Instances.Add(this); + } + + protected virtual void Update() { } + + protected virtual void Fix() + { + Instances.Remove(this); + } + + protected virtual string GetSuffix(PlayerControl seer, PlayerControl target, bool hud, bool meeting) + { + return string.Empty; + } + + + protected static int GetDefaultSabotageTimeLimit(MapNames map) + { + return map switch + { + MapNames.Skeld or MapNames.Dleks => 30, + MapNames.MiraHQ => 45, + MapNames.Polus => 60, + MapNames.Airship => 90, + MapNames.Fungle => 60, + _ => 75 + }; + } + + protected static void AdjustTimeLimitBasedOnPlayerSpeed(ref int timeLimit) + { + float speed = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); + timeLimit = (int)Math.Ceiling(timeLimit / speed); + } + + + public static string GetAllSuffix(PlayerControl seer, PlayerControl target, bool hud, bool meeting) + { + StringBuilder suffix = new(); + + foreach (CustomSabotage sabotage in Instances) + { + string tempSuffix = sabotage.GetSuffix(seer, target, hud, meeting).Trim(); + + if (!string.IsNullOrEmpty(tempSuffix)) + suffix.Append($"{tempSuffix}\n"); + } + + return suffix.ToString().Trim(); + } + + public static void UpdateAll() + { + foreach (CustomSabotage sabotage in Instances.ToArray()) + { + try { sabotage.Update(); } + catch (Exception e) { Utils.ThrowException(e); } + } + } + + public static void Reset() + { + Instances.Clear(); + } +} + +public class GrabOxygenMaskSabotage : CustomSabotage +{ + private long LastNotify; + private long LastReactorFlash; + private long DeteriorateTS; + private HashSet HasMask; + private SystemTypes TargetRoom; + private int TimeLimit; + private Vector2 RoomPosition; + + public override void Deteriorate() + { + base.Deteriorate(); + + DeteriorateTS = Utils.TimeStamp; + HasMask = []; + + MapNames map = Main.CurrentMap; + + TargetRoom = map switch + { + MapNames.Skeld or MapNames.Dleks => SystemTypes.MedBay, + MapNames.MiraHQ => SystemTypes.Balcony, + MapNames.Polus => SystemTypes.Specimens, + MapNames.Airship => SystemTypes.Medical, + MapNames.Fungle => SystemTypes.Kitchen, + _ => SystemTypes.Outside + }; + + TimeLimit = GetDefaultSabotageTimeLimit(map); + AdjustTimeLimitBasedOnPlayerSpeed(ref TimeLimit); + + RoomPosition = RandomSpawn.SpawnMap.GetSpawnMap().Positions.GetValueOrDefault(TargetRoom, TargetRoom.GetRoomClass().transform.position); + Main.AllAlivePlayerControls.Do(x => LocateArrow.Add(x.PlayerId, RoomPosition)); + } + + protected override void Update() + { + PlayerControl[] aapc = Main.AllAlivePlayerControls; + byte[] playersInRoom = aapc.Select(x => (id: x.PlayerId, room: x.GetPlainShipRoom())).Where(x => x.room != null && x.room.RoomId == TargetRoom).Select(x => x.id).ToArray(); + playersInRoom.Except(HasMask).ToValidPlayers().NotifyPlayers(Translator.GetString("CustomSabotage.GrabOxygenMask.Done")); + playersInRoom.Except(HasMask).Do(x => LocateArrow.Remove(x, RoomPosition)); + HasMask.UnionWith(playersInRoom); + + long now = Utils.TimeStamp; + + if (DeteriorateTS + TimeLimit <= now) + { + Main.AllPlayerControls.Do(x => LocateArrow.Remove(x.PlayerId, RoomPosition)); + aapc.ExceptBy(HasMask, x => x.PlayerId).Do(x => x.Suicide(PlayerState.DeathReason.OutOfOxygen)); + Fix(); + return; + } + + if (LastReactorFlash + 2 <= now) + { + LastReactorFlash = now; + aapc.ExceptBy(HasMask, x => x.PlayerId).Do(x => x.ReactorFlash(flashDuration: 0.1f)); + } + + if (LastNotify != now) + { + LastNotify = now; + Utils.NotifyRoles(); + } + } + + protected override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud, bool meeting) + { + if (seer.PlayerId != target.PlayerId || hud || meeting || HasMask.Contains(seer.PlayerId)) return string.Empty; + long now = Utils.TimeStamp; + return Utils.ColorString(now % 2 == 0 ? Color.yellow : Color.red, $"[{Translator.GetString(TargetRoom.ToString())}] {Translator.GetString("CustomSabotage.GrabOxygenMask")} {TimeLimit - (now - DeteriorateTS)} {LocateArrow.GetArrow(seer, RoomPosition)}"); + } +} \ No newline at end of file diff --git a/Modules/DoubleTrigger.cs b/Modules/DoubleTrigger.cs index d92f24d78..7a1a58a62 100644 --- a/Modules/DoubleTrigger.cs +++ b/Modules/DoubleTrigger.cs @@ -6,15 +6,12 @@ namespace EHR; internal static class DoubleTrigger { - public static List PlayerIdList = []; - public static Dictionary FirstTriggerTimer = []; public static Dictionary FirstTriggerTarget = []; public static Dictionary FirstTriggerAction = []; public static void Init() { - PlayerIdList = []; FirstTriggerTimer = []; FirstTriggerAction = []; } diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index f58969f77..5e5c104e7 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -1081,6 +1081,20 @@ internal static class ExtendedPlayerControl _ => SystemTypes.Reactor }; + if (IsActive(systemtypes)) + { + Main.PlayerStates[pc.PlayerId].IsBlackOut = true; + pc.MarkDirtySettings(); + + LateTask.New(() => + { + Main.PlayerStates[pc.PlayerId].IsBlackOut = false; + pc.MarkDirtySettings(); + }, (float.IsNaN(flashDuration) ? Options.KillFlashDuration.GetFloat() : flashDuration) + delay, "Fix BlackOut Reactor Flash"); + + return; + } + pc.RpcDesyncRepairSystem(systemtypes, 128); LateTask.New(() => diff --git a/Modules/FixedUpdateCaller.cs b/Modules/FixedUpdateCaller.cs index d71101a75..47b28d73f 100644 --- a/Modules/FixedUpdateCaller.cs +++ b/Modules/FixedUpdateCaller.cs @@ -80,12 +80,14 @@ public static class FixedUpdateCaller { NonLowLoadPlayerIndex++; - if (NonLowLoadPlayerIndex >= PlayerControl.AllPlayerControls.Count) - NonLowLoadPlayerIndex = Math.Min(0, -(30 - PlayerControl.AllPlayerControls.Count)); + int count = PlayerControl.AllPlayerControls.Count; + + if (NonLowLoadPlayerIndex >= count) + NonLowLoadPlayerIndex = Math.Min(0, -(30 - count)); CustomGameMode currentGameMode = Options.CurrentGameMode; - for (var index = 0; index < PlayerControl.AllPlayerControls.Count; index++) + for (var index = 0; index < count; index++) { try { diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index 04a52362f..90b19fd09 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -170,7 +170,7 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS SetMaxVision(); NaturalDisasters.ApplyGameOptions(opt, player.PlayerId); break; - case CustomGameMode.RoomRush when RoomRush.VentLimit[player.PlayerId] > 0: + case CustomGameMode.RoomRush when RoomRush.VentLimit.TryGetValue(player.PlayerId, out int vl) && vl > 0: AURoleOptions.EngineerCooldown = 0.01f; AURoleOptions.EngineerInVentMaxTime = 0f; goto case CustomGameMode.RoomRush; diff --git a/Modules/GameState.cs b/Modules/GameState.cs index 91772e87f..3fc0b3e19 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -66,6 +66,7 @@ public class PlayerState(byte playerId) Dragged, Mauled, WipedOut, + OutOfOxygen, // Natural Disasters Meteor, @@ -100,7 +101,7 @@ public class PlayerState(byte playerId) public DeathReason deathReason { get; set; } = DeathReason.etc; public bool IsBlackOut { get; set; } - public bool IsSuicide => deathReason == DeathReason.Suicide; + public bool IsSuicide => deathReason is DeathReason.Suicide or DeathReason.Fall; public TaskState TaskState { get; set; } = new(); public void SetMainRole(CustomRoles role, bool failsafe = false) @@ -166,7 +167,7 @@ public class PlayerState(byte playerId) RemoveDisableDevicesPatch.UpdateDisableDevices(); } - if (Lyncher.On) Lyncher.Instances.ForEach(x => x.OnRoleChange(PlayerId)); + if (Decryptor.On) Decryptor.Instances.ForEach(x => x.OnRoleChange(PlayerId)); if (!role.Is(Team.Impostor) && !(role == CustomRoles.Traitor && Traitor.CanGetImpostorOnlyAddons.GetBool())) SubRoles.ToArray().DoIf(x => x.IsImpOnlyAddon(), RemoveSubRole); diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs index 2b2ca45c7..a6899d14f 100644 --- a/Modules/GuessManager.cs +++ b/Modules/GuessManager.cs @@ -93,14 +93,14 @@ public static class GuessManager return true; } - if (pc.Is(CustomRoles.Lyncher) && Lyncher.GuessMode.GetValue() == 2) goto SkipCheck; + if (pc.Is(CustomRoles.Decryptor) && Decryptor.GuessMode.GetValue() == 2) goto SkipCheck; if ((pc.IsCrewmate() && !Options.CrewmatesCanGuess.GetBool()) || (pc.IsImpostor() && !Options.ImpostorsCanGuess.GetBool()) || (pc.Is(CustomRoleTypes.Coven) && !Options.CovenCanGuess.GetBool()) || (pc.IsNeutralKiller() && !Options.NeutralKillersCanGuess.GetBool()) || (pc.GetCustomRole().IsNonNK() && !Options.PassiveNeutralsCanGuess.GetBool()) || - (pc.Is(CustomRoles.Lyncher) && Lyncher.GuessMode.GetValue() == 0) || + (pc.Is(CustomRoles.Decryptor) && Decryptor.GuessMode.GetValue() == 0) || (Options.GuesserNumRestrictions.GetBool() && !Guessers.Contains(pc.PlayerId))) { if (pc.Is(CustomRoles.Guesser)) goto SkipCheck; @@ -113,7 +113,7 @@ public static class GuessManager SkipCheck: - if (pc.GetCustomRole() is CustomRoles.Lyncher or CustomRoles.NecroGuesser || + if (pc.GetCustomRole() is CustomRoles.Decryptor or CustomRoles.NecroGuesser || (pc.Is(CustomRoles.NiceGuesser) && Options.GGTryHideMsg.GetBool()) || (pc.Is(CustomRoles.EvilGuesser) && Options.EGTryHideMsg.GetBool()) || (pc.Is(CustomRoles.Doomsayer) && Doomsayer.DoomsayerTryHideMsg.GetBool()) || @@ -1082,7 +1082,7 @@ public static class GuessManager CustomRoles.NecroGuesser => true, CustomRoles.Augur => true, CustomRoles.Doomsayer when !Doomsayer.CantGuess => true, - CustomRoles.Lyncher when Lyncher.GuessMode.GetValue() == 2 => true, + CustomRoles.Decryptor when Decryptor.GuessMode.GetValue() == 2 => true, _ when Options.GuesserMode.GetBool() => lp.GetTeam() switch { Team.Impostor => Options.ImpostorsCanGuess.GetBool(), diff --git a/Modules/LocateArrow.cs b/Modules/LocateArrow.cs index 2b356bb6a..a9bab9ea3 100644 --- a/Modules/LocateArrow.cs +++ b/Modules/LocateArrow.cs @@ -101,6 +101,18 @@ internal static class LocateArrow return LocateArrows.Keys.Where(ai => ai.From == seer.PlayerId).Aggregate(string.Empty, (current, arrowInfo) => current + LocateArrows[arrowInfo]); } + /// + /// Get a specific visible target arrow + /// + /// + /// + /// + public static string GetArrow(PlayerControl seer, Vector3 position) + { + ArrowInfo arrowInfo = new(seer.PlayerId, position); + return LocateArrows.FirstOrDefault(a => a.Key.Equals(arrowInfo)).Value ?? string.Empty; + } + /// /// Check target arrow every FixedUpdate /// Issue NotifyRoles when there are updates diff --git a/Modules/Statistics.cs b/Modules/Statistics.cs index ad1734e8a..33d7e31bb 100644 --- a/Modules/Statistics.cs +++ b/Modules/Statistics.cs @@ -243,8 +243,8 @@ public static class Statistics if (exiled && exiledPlayer.PlayerId == lp.PlayerId && Main.PlayerStates.Values.Any(x => x.SubRoles.Contains(CustomRoles.Bait) && x.GetRealKiller() == lp.PlayerId)) Achievements.Type.Gotcha.Complete(); - if (lp.Is(CustomRoles.Lyncher) && Main.PlayerStates.Values.Count(x => x.deathReason == PlayerState.DeathReason.Gambled && x.GetRealKiller() == lp.PlayerId) >= 3) - Achievements.Type.GetLynched.Complete(); + if (lp.Is(CustomRoles.Decryptor) && Main.PlayerStates.Values.Count(x => x.deathReason == PlayerState.DeathReason.Gambled && x.GetRealKiller() == lp.PlayerId) >= 3) + Achievements.Type.GetDecrypted.Complete(); LateTask.New(() => { diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 11c862a6d..fdc8c179e 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -2268,17 +2268,19 @@ public static class Utils public static string BuildSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) { - string suffix = string.Empty; + StringBuilder suffix = new(); foreach (PlayerState state in Main.PlayerStates.Values) { string tempSuffix = state.Role.GetSuffix(seer, target, hud, meeting).Trim(); if (!string.IsNullOrEmpty(tempSuffix)) - suffix += $"{tempSuffix}\n"; + suffix.Append($"{tempSuffix}\n"); } - return suffix.Trim(); + suffix.Append(CustomSabotage.GetAllSuffix(seer, target, hud, meeting)); + + return suffix.ToString().Trim(); } public static IEnumerator NotifyEveryoneAsync(int speed = 2, bool noCache = true) diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index 0b9d2b928..700c6240d 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -76,6 +76,8 @@ internal static class GameEndChecker NameNotifyManager.Reset(); NotifyRoles(ForceLoop: true); + CustomSabotage.Reset(); + int saboWinner = Options.WhoWinsBySabotageIfNoImpAlive.GetValue(); if (reason == GameOverReason.ImpostorsBySabotage && saboWinner != 0 && !Main.AllAlivePlayerControls.Any(x => x.Is(Team.Impostor))) diff --git a/Patches/GameOptionsMenuPatch.cs b/Patches/GameOptionsMenuPatch.cs index 393f89e35..ed5fda602 100644 --- a/Patches/GameOptionsMenuPatch.cs +++ b/Patches/GameOptionsMenuPatch.cs @@ -680,7 +680,7 @@ public static class StringOptionPatch if (role.IsExperimental()) name += $"{Translator.GetString("ExperimentalRoleIndicator")}"; if (role.IsGhostRole()) name += GetGhostRoleTeam(role); - if (role.IsDevFavoriteRole()) name += "<#00ffff>★"; + if (role.IsDevFavoriteRole()) name += " <#00ffff>★"; __instance.TitleText.fontWeight = FontWeight.Black; __instance.TitleText.outlineColor = new(255, 255, 255, 255); @@ -822,7 +822,7 @@ public static class StringOptionPatch if (role.IsExperimental()) name += $"{Translator.GetString("ExperimentalRoleIndicator")}"; if (role.IsGhostRole()) name += GetGhostRoleTeam(role); - if (role.IsDevFavoriteRole()) name += "<#00ffff>★"; + if (role.IsDevFavoriteRole()) name += " <#00ffff>★"; __instance.TitleText.fontWeight = FontWeight.Black; __instance.TitleText.outlineColor = new(255, 255, 255, 255); diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index d2c1018f5..813906833 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -1050,9 +1050,7 @@ internal static class IntroCutsceneDestroyPatch if (Options.UsePets.GetBool() && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.HideAndSeek or CustomGameMode.CaptureTheFlag) { - Main.ProcessShapeshifts = false; - - LateTask.New(() => + void GrantPetForEveryone() { foreach (PlayerControl pc in aapc) { @@ -1063,7 +1061,13 @@ internal static class IntroCutsceneDestroyPatch PetsHelper.SetPet(pc, petId); Logger.Info($"{pc.GetNameWithRole()} => {GetString(petId)} Pet", "PetAssign"); } - }, 0.3f, "Grant Pet For Everyone"); + } + + Main.ProcessShapeshifts = false; + + LateTask.New(GrantPetForEveryone, 0.3f, "Grant Pet For Everyone"); + LateTask.New(GrantPetForEveryone, 0.7f, "Grant Pet For Everyone"); + LateTask.New(GrantPetForEveryone, 1.1f, "Grant Pet For Everyone"); LateTask.New(() => { @@ -1091,9 +1095,9 @@ internal static class IntroCutsceneDestroyPatch } catch (Exception ex) { Logger.Fatal(ex.ToString(), "IntroPatch.RpcShapeshift"); } } - }, 0.4f, "Show Pet For Everyone"); + }, 1.4f, "Show Pet For Everyone"); - LateTask.New(() => Main.ProcessShapeshifts = true, 1f, "Enable SS Processing"); + LateTask.New(() => Main.ProcessShapeshifts = true, 2f, "Enable SS Processing"); } try diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index 20ec29a7a..ad8d6d8f3 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -360,6 +360,7 @@ internal static class ChangeRoleSettings } FallFromLadder.Reset(); + CustomSabotage.Reset(); try { @@ -920,18 +921,21 @@ internal static class StartGameHostPatch { case CustomGameMode.HideAndSeek: CustomHnS.StartSeekerBlindTime(); - break; + goto default; case CustomGameMode.CaptureTheFlag: CaptureTheFlag.Init(); - break; + goto default; case CustomGameMode.NaturalDisasters: NaturalDisasters.OnGameStart(); break; case CustomGameMode.KingOfTheZones: KingOfTheZones.Init(); - break; + goto default; case CustomGameMode.Quiz: Quiz.Init(); + goto default; + default: + if (Options.IntegrateNaturalDisasters.GetBool()) goto case CustomGameMode.NaturalDisasters; break; } diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 388d4042b..6724a25bf 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -957,6 +957,12 @@ internal static class ReportDeadBodyPatch if (target == null) { + if (CustomSabotage.Instances.Count > 0) + { + Notify("CannotCallEmergencyMeetingWhileSabotage"); + return false; + } + if (__instance.Is(CustomRoles.Jester) && !Jester.JesterCanUseButton.GetBool()) { Notify("JesterCannotCallEmergencyMeeting"); @@ -1213,6 +1219,8 @@ internal static class ReportDeadBodyPatch PhantomRolePatch.OnReportDeadBody(pc); } + CustomSabotage.Reset(); + MeetingTimeManager.OnReportDeadBody(); NameNotifyManager.Reset(); @@ -1344,6 +1352,7 @@ internal static class FixedUpdatePatch if (localPlayer) { + CustomSabotage.UpdateAll(); Zoom.OnFixedUpdate(); TextBoxPatch.CheckChatOpen(); diff --git a/Patches/TextBoxPatch.cs b/Patches/TextBoxPatch.cs index bdd9f2770..4e31f50cb 100644 --- a/Patches/TextBoxPatch.cs +++ b/Patches/TextBoxPatch.cs @@ -316,7 +316,7 @@ public static class TextBoxPatch public static bool IsCharAllowedPatch(TextBoxTMP __instance, [HarmonyArgument(0)] char i, ref bool __result) { if (!__instance.gameObject.HasParentInHierarchy("ChatScreenRoot/ChatScreenContainer")) return true; - __result = i != '\b'; + __result = i is not '\b' and not '\n' and not '\r'; return false; } diff --git a/Resources/Config/EACList.txt b/Resources/Config/EACList.txt index cb0f12723..23d897720 100644 --- a/Resources/Config/EACList.txt +++ b/Resources/Config/EACList.txt @@ -98,6 +98,10 @@ afootauto#1216,026ad9f54,你妈阴道有蛆 rubykop#0410,a1de8a400,习主席近平 netgaff#6739,01b5992f0,李克强 fuckdaters#7129 +pucecoast#8105,d1134e5e5 +shoesilky#8411,c85846f8a +apemv#5959,77c89daa8 +beadyloaf#3586,9037e9c8f ## 冒充开发者 polkaoval#0453,自作Modな橋本環奈 diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index 85f0c39f0..bc794c679 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -1813,6 +1813,7 @@ "DoppelgangerWarning": "<#ffff00>The Doppelganger swapped skins with you!", "TriedToKillLastGameFirstKill": "Your target was the first to die in the last game.\\nLet them play!", "CheckReportFail": "<#ff0000>This meeting call was canceled due to the following reason:\\n", + "CannotCallEmergencyMeetingWhileSabotage": "Emergency meetings cannot be called during crisis", "JesterCannotCallEmergencyMeeting": "As the <#ec62a5>Jester, according to the host's settings, you cannot call emergency meetings", "NiceSwapperCannotCallEmergencyMeeting": "As the <#922348>Nice Swapper, according to the host's settings, you cannot call emergency meetings", "AdrenalineCannotCallEmergencyMeeting": "As the <#ffff00>Adrenaline, you cannot call emergency meetings currently", @@ -2074,6 +2075,9 @@ "StressedExtraTimeAfterFixingSabotage": "Time Gained For Fixing a Sabotage", "StressedExtraTimeAfterReporting": "Time Gained For Reporting a Dead Body", "StressedTimePenaltyAfterCrewmateEjected": "Time Penalty When a Crewmate Gets Ejected", + "CustomSabotageActivated": "<#ffff00>Custom Sabotage Activated!", + "CustomSabotage.GrabOxygenMask": "Grab Oxygen Mask", + "CustomSabotage.GrabOxygenMask.Done": "<#00ff00>Oxygen Mask Grabbed! You are safe.", "SabotageCooldownControl": "Modify Sabotage Cooldown", "SabotageCooldown": "Sabotage Cooldown", "SabotageTimeControl": "Modify Sabotage Time Limit", @@ -2813,7 +2817,7 @@ "Achievement.Speedrun.Description": "Have a game end in under 1 minute", "Achievement.Carried.Description": "Win as a Madmate while all other Impostors are dead", "Achievement.SorryToBurstYourBubble.Description": "Explode 5 people with 1 encased player as the Bubble", - "Achievement.GetLynched.Description": "Successfully guess 3 roles as Lyncher", + "Achievement.GetDecrypted.Description": "Successfully guess 3 roles as Decryptor", "Achievement.FlagMaster.Description": "Carry the flag the longest in Capture The Flag", "Achievement.Tag.Description": "Tag the most players in Capture The Flag", "Achievement.Vectory.Description": "Vent 50 times in one game", @@ -8959,12 +8963,12 @@ "SleepInfo": "You Sleep During Lights Out", "SleepInfoLong": "(Add-ons):\\nYou sleep during lights out. You can't see anything, and you cannot move. You can only wake up when the lights are back on. If someone with the Glow add-on comes near you, you fully wake up and lose this add-on.", "Prefix.Sleep": "Sleeping", - "Lyncher": "Lyncher", - "LyncherInfo": "Get Letters From Everyone's Roles", - "LyncherInfoLong": "(Crewmates):\\nAs the Lyncher, each X tasks you complete (X is set by the host), you get to know one more letter from everyone's roles. You can see these characters below players' names. The letters are not in order and each letter only appears once for every occurance in the role name. Settings determine whether you can guess.", - "Lyncher.TaskNum": "Number of Tasks to Complete for Each Letter", - "Lyncher.GuessMode": "Can Guess", - "Lyncher.Suffix": "<#00ffa5>Complete {0} <#00ffa5>tasks for the next letters", + "Decryptor": "Decryptor", + "DecryptorInfo": "Get Letters From Everyone's Roles", + "DecryptorInfoLong": "(Crewmates):\\nAs the Decryptor, each X tasks you complete (X is set by the host), you get to know one more letter from everyone's roles. You can see these characters below players' names. The letters are not in order and each letter only appears once for every occurance in the role name. Settings determine whether you can guess.", + "Decryptor.TaskNum": "Number of Tasks to Complete for Each Letter", + "Decryptor.GuessMode": "Can Guess", + "Decryptor.Suffix": "<#00ffa5>Complete {0} <#00ffa5>tasks for the next letters", "ParityCopTryHideMsg": "Hide Inspector's commands", "MaxParityCheckLimit": "Max inspections per game", "ParityCheckLimitPerMeeting": "Max inspections per meeting", diff --git a/Roles/Crewmate/Lyncher.cs b/Roles/Crewmate/Lyncher.cs index d11a921f7..f9cc6cb3b 100644 --- a/Roles/Crewmate/Lyncher.cs +++ b/Roles/Crewmate/Lyncher.cs @@ -6,10 +6,10 @@ using Hazel; namespace EHR.Crewmate; -public class Lyncher : RoleBase +public class Decryptor : RoleBase { public static bool On; - public static List Instances = []; + public static List Instances = []; private static OptionItem TaskNum; public static OptionItem GuessMode; @@ -25,7 +25,7 @@ public class Lyncher : RoleBase private static Dictionary> AllRoleNames = []; private Dictionary> KnownCharacters = []; - private byte LyncherId; + private byte DecryptorId; private int TasksCompleted; public override bool IsEnable => On; @@ -33,19 +33,19 @@ public class Lyncher : RoleBase public override void SetupCustomOption() { var id = 648550; - Options.SetupRoleOptions(id++, TabGroup.CrewmateRoles, CustomRoles.Lyncher); + Options.SetupRoleOptions(id++, TabGroup.CrewmateRoles, CustomRoles.Decryptor); - TaskNum = new IntegerOptionItem(++id, "Lyncher.TaskNum", new(1, 10, 1), 3, TabGroup.CrewmateRoles) - .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Lyncher]); + TaskNum = new IntegerOptionItem(++id, "Decryptor.TaskNum", new(1, 10, 1), 3, TabGroup.CrewmateRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Decryptor]); - GuessMode = new StringOptionItem(++id, "Lyncher.GuessMode", GuessModes, 1, TabGroup.CrewmateRoles) - .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Lyncher]); + GuessMode = new StringOptionItem(++id, "Decryptor.GuessMode", GuessModes, 1, TabGroup.CrewmateRoles) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Decryptor]); Vision = new FloatOptionItem(++id, "Vision", new(0f, 1.5f, 0.05f), 0.5f, TabGroup.CrewmateRoles) .SetValueFormat(OptionFormat.Multiplier) - .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Lyncher]); + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Decryptor]); - Options.OverrideTasksData.Create(++id, TabGroup.CrewmateRoles, CustomRoles.Lyncher); + Options.OverrideTasksData.Create(++id, TabGroup.CrewmateRoles, CustomRoles.Decryptor); } public override void Init() @@ -61,9 +61,9 @@ public class Lyncher : RoleBase { On = true; Instances.Add(this); - LyncherId = playerId; + DecryptorId = playerId; TasksCompleted = 0; - Utils.SendRPC(CustomRPC.SyncRoleData, LyncherId, 1, TasksCompleted); + Utils.SendRPC(CustomRPC.SyncRoleData, DecryptorId, 1, TasksCompleted); if (Main.HasJustStarted) LateTask.New(Action, 12f, log: false); @@ -102,7 +102,7 @@ public class Lyncher : RoleBase else Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc); - Utils.SendRPC(CustomRPC.SyncRoleData, LyncherId, 1, TasksCompleted); + Utils.SendRPC(CustomRPC.SyncRoleData, DecryptorId, 1, TasksCompleted); } private void RevealLetter() @@ -112,18 +112,18 @@ public class Lyncher : RoleBase KnownCharacters.Do(x => { x.Value.Add(nextLetters[x.Key]); - Utils.SendRPC(CustomRPC.SyncRoleData, LyncherId, 2, x.Key, nextLetters[x.Key]); + Utils.SendRPC(CustomRPC.SyncRoleData, DecryptorId, 2, x.Key, nextLetters[x.Key]); }); } public void OnRoleChange(byte id) { - Utils.SendRPC(CustomRPC.SyncRoleData, LyncherId, 3, id); + Utils.SendRPC(CustomRPC.SyncRoleData, DecryptorId, 3, id); CustomRoles newRole = Main.PlayerStates[id].MainRole; AllRoleNames[id] = Translator.GetString($"{newRole}").ToUpper().Where(c => c is not '-' and not ' ').Shuffle(); int count = KnownCharacters[id].Count; KnownCharacters[id] = AllRoleNames[id].Take(count).ToList(); - KnownCharacters[id].ForEach(x => Utils.SendRPC(CustomRPC.SyncRoleData, LyncherId, 2, id, x)); + KnownCharacters[id].ForEach(x => Utils.SendRPC(CustomRPC.SyncRoleData, DecryptorId, 2, id, x)); } public void ReceiveRPC(MessageReader reader) @@ -144,12 +144,12 @@ public class Lyncher : RoleBase public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false) { - if (seer.PlayerId != LyncherId) return string.Empty; + if (seer.PlayerId != DecryptorId) return string.Empty; return (seer.PlayerId == target.PlayerId) switch { false when KnownCharacters.TryGetValue(target.PlayerId, out List chars) && chars.Count > 0 => string.Join(' ', chars), - true when !seer.IsModdedClient() || hud => string.Format(Translator.GetString("Lyncher.Suffix"), TaskNum.GetInt() - TasksCompleted), + true when !seer.IsModdedClient() || hud => string.Format(Translator.GetString("Decryptor.Suffix"), TaskNum.GetInt() - TasksCompleted), _ => string.Empty }; } diff --git a/Roles/Crewmate/Monarch.cs b/Roles/Crewmate/Monarch.cs index 24bb0336c..29d50bd88 100644 --- a/Roles/Crewmate/Monarch.cs +++ b/Roles/Crewmate/Monarch.cs @@ -94,7 +94,7 @@ public class Monarch : RoleBase sender.SendMessage(!hasValue); - Logger.Info("SetRole:" + target.Data?.PlayerName + " = " + target.GetCustomRole() + " + " + CustomRoles.Knighted, "Assign " + CustomRoles.Knighted); + Logger.Info($"Set Role: {target.Data?.PlayerName} = {target.GetCustomRole()} + {CustomRoles.Knighted}", $"Assign {CustomRoles.Knighted}"); return false; } diff --git a/Roles/Neutral/Gaslighter.cs b/Roles/Neutral/Gaslighter.cs index e13f53215..83f331ee0 100644 --- a/Roles/Neutral/Gaslighter.cs +++ b/Roles/Neutral/Gaslighter.cs @@ -175,7 +175,7 @@ public class Gaslighter : RoleBase string role = IRandom.Instance.Next(2) == 0 ? CustomRoles.HexMaster.ToColoredString() : CustomRoles.Witch.ToColoredString(); string text = string.Format(Translator.GetString("WitchCursedPlayersMessage"), cursed, role); Utils.SendMessage(text, title: Translator.GetString("MessageTitle.Attention")); - }, 10f, "Witch Cursed Players Notify"); + }, 10f, "Gaslighter Cursed Players Notify"); } } @@ -187,8 +187,10 @@ public class Gaslighter : RoleBase return true; case Round.Knight when killer.GetAbilityUseLimit() > 0 && !target.Is(CustomRoles.Knighted): target.RpcSetCustomRole(CustomRoles.Knighted); + target.RpcGuardAndKill(killer); + target.RpcGuardAndKill(target); + target.Notify(Utils.ColorString(Utils.GetRoleColor(CustomRoles.Monarch), Translator.GetString("KnightedByMonarch"))); Utils.NotifyRoles(SpecifySeer: killer, SpecifyTarget: target); - Utils.NotifyRoles(SpecifySeer: target, SpecifyTarget: target); killer.RpcRemoveAbilityUse(); killer.SetKillCooldown(); break;