From dd50ae4caead02b53e2174b5472dad0113ed2715 Mon Sep 17 00:00:00 2001 From: Christina <122680126+HayashiUme@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:29:09 +0800 Subject: [PATCH] New game mode: LoopWanted (Cycle Hunt Order) (#631) * Add new Gamemode and fix SettingMenu button order issue * back path --- CTA/CustomTeamAssigner/CustomRoles.cs | 3 + CustomRoles.cs | 3 + EHR.csproj | 2 +- Gamemodes/LoopWanted.cs | 442 ++++++++++++++++++ Main.cs | 6 +- Modules/CustomRoleSelector.cs | 3 +- Modules/CustomRolesHelper.cs | 7 +- Modules/ExtendedPlayerControl.cs | 5 +- Modules/FixedUpdateCaller.cs | 3 + .../PlayerGameOptionsSender.cs | 5 + Modules/NameColorManager.cs | 1 + Modules/OptionHolder.cs | 7 +- Modules/RPC.cs | 8 +- Modules/Utils.cs | 14 +- Patches/CheckGameEndPatch.cs | 19 + Patches/GameOptionsMenuPatch.cs | 7 +- Patches/IntroPatch.cs | 22 + Patches/OnGameStartedPatch.cs | 7 + Patches/OutroPatch.cs | 3 +- Patches/PlayerControlPatch.cs | 9 +- Resources/Lang/en_US.jsonc | 22 + Resources/Lang/zh_CN.jsonc | 22 + 22 files changed, 605 insertions(+), 15 deletions(-) create mode 100644 Gamemodes/LoopWanted.cs diff --git a/CTA/CustomTeamAssigner/CustomRoles.cs b/CTA/CustomTeamAssigner/CustomRoles.cs index 183014ddf..58b98b3bc 100644 --- a/CTA/CustomTeamAssigner/CustomRoles.cs +++ b/CTA/CustomTeamAssigner/CustomRoles.cs @@ -504,6 +504,9 @@ public enum CustomRoles // Snowdown SnowdownPlayer, + // LoopWanted + LoopHunter, + // GM GM, diff --git a/CustomRoles.cs b/CustomRoles.cs index 183014ddf..58b98b3bc 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -504,6 +504,9 @@ public enum CustomRoles // Snowdown SnowdownPlayer, + // LoopWanted + LoopHunter, + // GM GM, diff --git a/EHR.csproj b/EHR.csproj index 32f8bd2b2..f470574e4 100644 --- a/EHR.csproj +++ b/EHR.csproj @@ -1,7 +1,7 @@  - D:\SteamLibrary\steamapps\common\Among Us + D:\SteamLibrary\steamapps\common\Among Us diff --git a/Gamemodes/LoopWanted.cs b/Gamemodes/LoopWanted.cs new file mode 100644 index 000000000..b7749410f --- /dev/null +++ b/Gamemodes/LoopWanted.cs @@ -0,0 +1,442 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AmongUs.GameOptions; +using EHR.Modules; +using Hazel; +using UnityEngine; +using static EHR.Translator; + +namespace EHR.Gamemodes; + +internal static class LoopWanted +{ + private static Dictionary TargetMap = []; + private static Dictionary LowerVisionList = []; + private static bool CarnivalMode; + private static float DefaultSpeed; + + private static OptionItem BaseKillCooldown; + private static OptionItem PunishmentMode; + private static OptionItem LowerVisionMultiplier; + private static OptionItem CarnivalPlayerCount; + private static OptionItem CarnivalKillCooldown; + private static OptionItem CarnivalSpeedMultiplier; + private static OptionItem ShowTargetArrow; + + private static readonly string[] PunishmentModeOptions = ["LoopWanted.PunishSuicide", "LoopWanted.PunishLowerVision"]; + + public static void SetupCustomOption() + { + var id = 69_226_001; + Color color = new Color32(255, 180, 50, byte.MaxValue); + const CustomGameMode gameMode = CustomGameMode.LoopWanted; + const TabGroup tab = TabGroup.GameSettings; + + BaseKillCooldown = new FloatOptionItem(id++, "LoopWanted.BaseKillCooldown", new(2.5f, 60f, 2.5f), 15f, tab) + .SetHeader(true) + .SetGameMode(gameMode) + .SetColor(color) + .SetValueFormat(OptionFormat.Seconds); + + PunishmentMode = new StringOptionItem(id++, "LoopWanted.PunishmentMode", PunishmentModeOptions, 0, tab) + .SetGameMode(gameMode) + .SetColor(color) + .RegisterUpdateValueEvent((_, _, currentValue) => + { + LowerVisionMultiplier.SetHidden(currentValue != 1); + }) + .SetRunEventOnLoad(true); + + LowerVisionMultiplier = new FloatOptionItem(id++, "LoopWanted.LowerVisionMultiplier", new(0f, 10f, 0.5f), 0.5f, tab) + .SetParent(PunishmentMode) + .SetGameMode(gameMode) + .SetColor(color) + .SetValueFormat(OptionFormat.Multiplier); + + CarnivalPlayerCount = new IntegerOptionItem(id++, "LoopWanted.CarnivalPlayerCount", new(1, 127, 1), 3, tab) + .SetGameMode(gameMode) + .SetColor(color) + .SetValueFormat(OptionFormat.Players); + + CarnivalKillCooldown = new FloatOptionItem(id++, "LoopWanted.CarnivalKillCooldown", new(2.5f, 60f, 2.5f), 5f, tab) + .SetGameMode(gameMode) + .SetColor(color) + .SetValueFormat(OptionFormat.Seconds); + + CarnivalSpeedMultiplier = new FloatOptionItem(id++, "LoopWanted.CarnivalSpeedMultiplier", new(0.25f, 10f, 0.25f), 2.0f, tab) + .SetGameMode(gameMode) + .SetColor(color) + .SetValueFormat(OptionFormat.Multiplier); + + ShowTargetArrow = new BooleanOptionItem(id++, "LoopWanted.ShowTargetArrow", false, tab) + .SetGameMode(gameMode) + .SetColor(color); + } + + public static void Init() + { + if (Options.CurrentGameMode != CustomGameMode.LoopWanted) return; + + TargetMap = []; + LowerVisionList = []; + CarnivalMode = false; + + DefaultSpeed = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod); + + Utils.SendRPC(CustomRPC.LoopWantedSync, 1, false); + } + + public static void OnGameStart() + { + if (Options.CurrentGameMode != CustomGameMode.LoopWanted) return; + AssignTargets(); + } + + private static void AssignTargets() + { + if (!AmongUsClient.Instance.AmHost) return; + + TargetMap = []; + TargetArrow.Init(); + + var allPlayers = Main.EnumerateAlivePlayerControls(); + if (Main.GM.Value) + allPlayers = allPlayers.Without(PlayerControl.LocalPlayer); + allPlayers = allPlayers.ExceptBy(ChatCommands.Spectators, x => x.PlayerId).ToArray(); + + List playerIds = allPlayers.Select(x => x.PlayerId).Shuffle().ToList(); + + if (playerIds.Count < 2) + { + Logger.Warn("Not enough players for LoopWanted", "LoopWanted"); + return; + } + + for (int i = 0; i < playerIds.Count; i++) + { + byte targetId = playerIds[(i + 1) % playerIds.Count]; + byte hunterId = playerIds[i]; + if (hunterId == targetId) continue; // should never happen, but guard + if (TargetMap.ContainsValue(targetId)) + TargetMap.Remove(TargetMap.First(kv => kv.Value == targetId).Key); + TargetMap[hunterId] = targetId; + if (ShowTargetArrow.GetBool()) TargetArrow.Add(hunterId, targetId); + Logger.Info($"{hunterId.ColoredPlayerName()} 2 targets 2 {targetId.ColoredPlayerName()}", "LoopWanted"); + } + + SyncTargetMapRPC(); + CarnivalMode = false; + } + + public static byte GetTarget(byte playerId) + { + return TargetMap.TryGetValue(playerId, out byte target) ? target : byte.MaxValue; + } + + public static bool OnCheckMurder(PlayerControl killer, PlayerControl target) + { + if (!AmongUsClient.Instance.AmHost) return false; + + byte killerId = killer.PlayerId; + byte targetId = target.PlayerId; + + byte expectedTarget = GetTarget(killerId); + + if (expectedTarget == targetId) + { + Logger.Info($"{killer.GetRealName()} correctly killed their target {target.GetRealName()}", "LoopWanted"); + + byte nextTarget = GetTarget(targetId); + + TargetArrow.RemoveAllTarget(targetId); + RemoveTargetArrowsTo(targetId); + + if (nextTarget != byte.MaxValue && nextTarget != killerId) + { + if (TargetMap.ContainsValue(nextTarget)) + TargetMap.Remove(TargetMap.First(kv => kv.Value == nextTarget).Key); + TargetMap[killerId] = nextTarget; + if (ShowTargetArrow.GetBool()) TargetArrow.Add(killerId, nextTarget); + Logger.Info($"{killer.GetRealName()}'s new target is {nextTarget.ColoredPlayerName()}", "LoopWanted"); + } + else + { + TargetMap.Remove(killerId); + TargetArrow.RemoveAllTarget(killerId); + } + + TargetMap.Remove(targetId); + + killer.SyncSettings(); + + CheckCarnivalMode(); + Utils.NotifyRoles(SpecifySeer: killer); + + SyncTargetMapRPC(); + Utils.SendRPC(CustomRPC.LoopWantedSync, 1, CarnivalMode); + + killer.Kill(target); + return false; + } + else + { + Logger.Info($"{killer.GetRealName()} killed wrong target {target.GetRealName()} (expected: {expectedTarget.ColoredPlayerName()})", "LoopWanted"); + int punishment = PunishmentMode.GetInt(); + if (punishment == 0) + { + killer.Kill(target); + ApplyPunishment(killer); + } + else + { + ApplyPunishment(killer); + } + return false; + } + } + + private static void RemoveTargetArrowsTo(byte targetId) + { + foreach (var kvp in TargetMap) + { + if (kvp.Value == targetId) + TargetArrow.RemoveAllTarget(kvp.Key); + } + } + + private static void ApplyPunishment(PlayerControl killer) + { + int punishment = PunishmentMode.GetInt(); + + switch (punishment) + { + case 0: + Logger.Info($"{killer.GetRealName()} is committing suicide for wrong kill", "LoopWanted"); + killer.Notify(GetString("LoopWanted.WrongKillSuicide"), 5f); + LateTask.New(() => + { + if (!killer || !killer.IsAlive() || GameStates.IsEnded) return; + killer.Suicide(PlayerState.DeathReason.Kill); + HandleDeadPlayer(killer.PlayerId); + }, 0.5f, "LoopWanted Suicide Punishment"); + break; + + case 1: + float visionMultiplier = LowerVisionMultiplier.GetFloat(); + LowerVisionList[killer.PlayerId] = Utils.TimeStamp; + killer.MarkDirtySettings(); + killer.Notify(string.Format(GetString("LoopWanted.WrongKillLowerVision"), visionMultiplier), 5f); + break; + } + } + + public static void HandleDeadPlayer(byte deadPlayerId) + { + if (!AmongUsClient.Instance.AmHost) return; + if (!TargetMap.ContainsKey(deadPlayerId)) return; + + byte hunterId = byte.MaxValue; + foreach (var kvp in TargetMap) + { + if (kvp.Value == deadPlayerId && kvp.Key != deadPlayerId) + { + hunterId = kvp.Key; + break; + } + } + + byte nextTarget = GetTarget(deadPlayerId); + + TargetArrow.RemoveAllTarget(deadPlayerId); + TargetMap.Remove(deadPlayerId); + LowerVisionList.Remove(deadPlayerId); + + if (hunterId != byte.MaxValue && nextTarget != byte.MaxValue && nextTarget != hunterId) + { + TargetArrow.RemoveAllTarget(hunterId); + if (TargetMap.ContainsValue(nextTarget)) + TargetMap.Remove(TargetMap.First(kv => kv.Value == nextTarget).Key); + TargetMap[hunterId] = nextTarget; + if (ShowTargetArrow.GetBool()) TargetArrow.Add(hunterId, nextTarget); + + PlayerControl hunter = Utils.GetPlayerById(hunterId); + if (hunter) Utils.NotifyRoles(SpecifySeer: hunter); + } + else if (hunterId != byte.MaxValue) + { + TargetMap.Remove(hunterId); + TargetArrow.RemoveAllTarget(hunterId); + } + + CheckCarnivalMode(); + SyncTargetMapRPC(); + } + + private static void CheckCarnivalMode() + { + int aliveCount = Main.EnumerateAlivePlayerControls().Count(x => !ChatCommands.Spectators.Contains(x.PlayerId)); + int threshold = CarnivalPlayerCount.GetInt(); + + if (!CarnivalMode && aliveCount <= threshold && aliveCount > 1) + { + CarnivalMode = true; + Utils.SendRPC(CustomRPC.LoopWantedSync, 1, true); + + foreach (PlayerControl pc in Main.EnumerateAlivePlayerControls()) + { + if (ChatCommands.Spectators.Contains(pc.PlayerId)) continue; + + Main.AllPlayerSpeed[pc.PlayerId] = DefaultSpeed * CarnivalSpeedMultiplier.GetFloat(); + Main.AllPlayerKillCooldown[pc.PlayerId] = CarnivalKillCooldown.GetFloat(); + pc.SyncSettings(); + pc.MarkDirtySettings(); + pc.Notify(GetString("LoopWanted.CarnivalModeActivated"), 5f); + } + + SoundManager.Instance.PlaySound(HudManager.Instance.LobbyTimerExtensionUI.lobbyTimerPopUpSound, false); + Utils.FlashColor(new(1f, 0.5f, 0f, 0.4f), 1.4f); + Logger.Info($"Carnival Mode activated! {aliveCount} players remaining.", "LoopWanted"); + } + } + + public static bool CheckForGameEnd(out GameOverReason reason) + { + reason = GameOverReason.ImpostorsByKill; + + if (!Main.IntroDestroyed || GameStates.IsEnded) return false; + + var alivePlayers = Main.EnumerateAlivePlayerControls() + .Where(x => !ChatCommands.Spectators.Contains(x.PlayerId)).ToArray(); + + if (alivePlayers.Length <= 1) + { + if (alivePlayers.Length == 1) + { + PlayerControl winner = alivePlayers[0]; + CustomWinnerHolder.WinnerIds = [winner.PlayerId]; + Logger.Info($"LoopWanted Winner: {winner.GetRealName()}", "LoopWanted"); + } + else + { + CustomWinnerHolder.WinnerIds = []; + } + + return true; + } + + return false; + } + + public static string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false) + { + if (seer.PlayerId != target.PlayerId || GameStates.IsMeeting) return string.Empty; + + byte targetId = GetTarget(seer.PlayerId); + if (targetId == byte.MaxValue) return string.Empty; + + string targetName = Main.AllPlayerNames.GetValueOrDefault(targetId, GetString("Unknown")); + string arrow = ShowTargetArrow.GetBool() ? TargetArrow.GetArrows(seer, targetId) : string.Empty; + + var sb = new StringBuilder(); + sb.Append($"{(hud ? GetString("LoopWanted.CurrentTarget") : GetString("Target"))}: {targetName.RemoveHtmlTags().Replace("\r\n", string.Empty)} {arrow}"); + + if (CarnivalMode) + sb.Append($"\n{GetString("LoopWanted.CarnivalModeHUD")}"); + + return sb.ToString(); + } + + private static void SyncTargetMapRPC() + { + if (!AmongUsClient.Instance.AmHost) return; + + var writer = Utils.CreateRPC(CustomRPC.LoopWantedSync); + writer.Write(2); + writer.Write(TargetMap.Count); + foreach (var kvp in TargetMap) + { + writer.Write(kvp.Key); + writer.Write(kvp.Value); + } + Utils.EndRPC(writer); + } + + public static void ReceiveRPC(MessageReader reader) + { + switch (reader.ReadPackedInt32()) + { + case 1: + CarnivalMode = reader.ReadBoolean(); + break; + case 2: + TargetMap = []; + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + TargetMap[reader.ReadByte()] = reader.ReadByte(); + break; + } + } + + public static void ApplyGameOptions(IGameOptions opt, byte playerId) + { + AURoleOptions.PhantomCooldown = 0.1f; + + try { AURoleOptions.GuardianAngelCooldown = 900f; } + catch (Exception e) { Utils.ThrowException(e); } + + if (LowerVisionList.ContainsKey(playerId)) + { + opt.SetVision(false); + opt.SetFloat(FloatOptionNames.CrewLightMod, LowerVisionMultiplier.GetFloat()); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, LowerVisionMultiplier.GetFloat()); + } + else + { + opt.SetVision(true); + opt.SetFloat(FloatOptionNames.CrewLightMod, 1.3f); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, 1.3f); + } + } + + public static class FixedUpdatePatch + { + private static long LastFixedUpdate; + + public static void Postfix() + { + if (!Main.IntroDestroyed || !GameStates.IsInTask || ExileController.Instance || + Options.CurrentGameMode != CustomGameMode.LoopWanted || !AmongUsClient.Instance.AmHost) return; + + long now = Utils.TimeStamp; + if (IntroCutsceneDestroyPatch.IntroDestroyTS + 10 > now || LastFixedUpdate == now) return; + LastFixedUpdate = now; + + var deadPlayersInMap = new List(); + foreach (var kvp in TargetMap) + { + byte id = kvp.Key; + if (id.GetPlayer()?.IsAlive() == true) continue; + if (Main.PlayerStates.TryGetValue(id, out var state) && state.IsDead) + deadPlayersInMap.Add(id); + } + foreach (byte deadId in deadPlayersInMap) + HandleDeadPlayer(deadId); + + var orphaned = new List(); + foreach (var kvp in TargetMap) + { + byte targetId = kvp.Value; + var targetPlayer = targetId.GetPlayer(); + var state = Main.PlayerStates.GetValueOrDefault(targetId); + if (targetPlayer && !targetPlayer.IsAlive() || (state != null && state.IsDead)) + orphaned.Add(kvp.Key); + } + foreach (byte hunterId in orphaned) + HandleDeadPlayer(TargetMap[hunterId]); + + CheckCarnivalMode(); + } + } +} diff --git a/Main.cs b/Main.cs index 70f0a9608..8975eb941 100644 --- a/Main.cs +++ b/Main.cs @@ -147,7 +147,8 @@ public class Main : BasePlugin [CustomGameMode.Speedrun] = [], [CustomGameMode.CaptureTheFlag] = [], [CustomGameMode.NaturalDisasters] = [], - [CustomGameMode.Snowdown] = [] + [CustomGameMode.Snowdown] = [], + [CustomGameMode.LoopWanted] = [] }; public static Dictionary GameModeColors = []; @@ -972,7 +973,8 @@ public class Main : BasePlugin [CustomGameMode.BedWars] = Utils.GetRoleColor(CustomRoles.BedWarsPlayer), [CustomGameMode.Deathrace] = Utils.GetRoleColor(CustomRoles.Racer), [CustomGameMode.Mingle] = Utils.GetRoleColor(CustomRoles.MinglePlayer), - [CustomGameMode.Snowdown] = Utils.GetRoleColor(CustomRoles.SnowdownPlayer) + [CustomGameMode.Snowdown] = Utils.GetRoleColor(CustomRoles.SnowdownPlayer), + [CustomGameMode.LoopWanted] = Utils.GetRoleColor(CustomRoles.LoopHunter) }; IL2CPPChainloader.Instance.Finished += () => diff --git a/Modules/CustomRoleSelector.cs b/Modules/CustomRoleSelector.cs index bd3536122..e569320d0 100644 --- a/Modules/CustomRoleSelector.cs +++ b/Modules/CustomRoleSelector.cs @@ -36,7 +36,8 @@ internal static class CustomRoleSelector { CustomGameMode.BedWars, CustomRoles.BedWarsPlayer }, { CustomGameMode.Deathrace, CustomRoles.Racer }, { CustomGameMode.Mingle, CustomRoles.MinglePlayer }, - { CustomGameMode.Snowdown, CustomRoles.SnowdownPlayer } + { CustomGameMode.Snowdown, CustomRoles.SnowdownPlayer }, + { CustomGameMode.LoopWanted, CustomRoles.LoopHunter } }; public static void SelectCustomRoles() diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 1322c5256..82ce63ccf 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -74,7 +74,8 @@ internal static class CustomRolesHelper CustomRoles.BedWarsPlayer or CustomRoles.Racer or CustomRoles.MinglePlayer or - CustomRoles.SnowdownPlayer; + CustomRoles.SnowdownPlayer or + CustomRoles.LoopHunter; } public RoleBase GetRoleClass() @@ -434,6 +435,8 @@ internal static class CustomRolesHelper CustomRoles.TMGPlayer => CustomRoles.Crewmate, // Mingle CustomRoles.MinglePlayer => CustomRoles.Crewmate, + // LoopWanted + CustomRoles.LoopHunter => CustomRoles.Impostor, _ => role.IsImpostor() ? CustomRoles.Impostor : CustomRoles.Crewmate }; @@ -486,6 +489,8 @@ internal static class CustomRolesHelper CustomRoles.Racer => RoleTypes.Phantom, // Snowdown CustomRoles.SnowdownPlayer => RoleTypes.Phantom, + // LoopWanted + CustomRoles.LoopHunter => RoleTypes.Impostor, // Standard CustomRoles.Sheriff => UsePets && Sheriff.UsePet.GetBool() ? RoleTypes.GuardianAngel : RoleTypes.Impostor, CustomRoles.Crusader => UsePets && Crusader.UsePet.GetBool() ? RoleTypes.GuardianAngel : RoleTypes.Impostor, diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index 760c94930..db5a2b97a 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -373,7 +373,7 @@ internal static class ExtendedPlayerControl RoleTypes newRoleType = state.MainRole.GetRoleTypes(); CustomGameMode gameMode = Options.CurrentGameMode; - if (gameMode is CustomGameMode.SoloPVP or CustomGameMode.FFA or CustomGameMode.CaptureTheFlag or CustomGameMode.KingOfTheZones or CustomGameMode.BedWars or CustomGameMode.Snowdown) + if (gameMode is CustomGameMode.SoloPVP or CustomGameMode.FFA or CustomGameMode.CaptureTheFlag or CustomGameMode.KingOfTheZones or CustomGameMode.BedWars or CustomGameMode.Snowdown or CustomGameMode.LoopWanted) hasValue |= sender.RpcSetRole(player, newRoleType, player.OwnerId); player.ResetKillCooldown(); @@ -1338,6 +1338,7 @@ internal static class ExtendedPlayerControl case CustomGameMode.BedWars: case CustomGameMode.Deathrace: case CustomGameMode.Snowdown: + case CustomGameMode.LoopWanted: return true; } @@ -1398,6 +1399,7 @@ internal static class ExtendedPlayerControl CustomGameMode.TheMindGame => false, CustomGameMode.Mingle => false, CustomGameMode.Snowdown => true, + CustomGameMode.LoopWanted => false, CustomGameMode.Deathrace => Deathrace.CanUseVent(player, player.GetClosestVent().Id), CustomGameMode.Standard when CopyCat.PlayerIdList.Contains(player.PlayerId) => true, @@ -1783,6 +1785,7 @@ internal static class ExtendedPlayerControl CustomRoles.BedWarsPlayer => 1f, CustomRoles.Racer => 3f, CustomRoles.SnowdownPlayer => 10f, + CustomRoles.LoopHunter => Main.AllPlayerKillCooldown[player.PlayerId], _ when player.Is(CustomRoles.Underdog) => Main.AllAlivePlayerControlsCount <= Underdog.UnderdogMaximumPlayersNeededToKill.GetInt() ? Underdog.UnderdogKillCooldownWithLessPlayersAlive.GetInt() : Underdog.UnderdogKillCooldownWithMorePlayersAlive.GetInt(), _ => Main.AllPlayerKillCooldown[player.PlayerId] }; diff --git a/Modules/FixedUpdateCaller.cs b/Modules/FixedUpdateCaller.cs index 204af5057..12cbe2065 100644 --- a/Modules/FixedUpdateCaller.cs +++ b/Modules/FixedUpdateCaller.cs @@ -162,6 +162,9 @@ public static class FixedUpdateCaller case CustomGameMode.Snowdown: Snowdown.FixedUpdatePatch.Postfix(pc); break; + case CustomGameMode.LoopWanted: + LoopWanted.FixedUpdatePatch.Postfix(); + break; } CheckInvalidMovementPatch.Postfix(pc); diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index e3d899089..b2f30a0ec 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -389,6 +389,11 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS Snowdown.ApplyGameOptions(); goto case CustomGameMode.RoomRush; } + case CustomGameMode.LoopWanted: + { + LoopWanted.ApplyGameOptions(opt, player.PlayerId); + break; + } case CustomGameMode.NaturalDisasters: { SetMaxVision(); diff --git a/Modules/NameColorManager.cs b/Modules/NameColorManager.cs index 119ed338d..5c4fbcd48 100644 --- a/Modules/NameColorManager.cs +++ b/Modules/NameColorManager.cs @@ -43,6 +43,7 @@ public static class NameColorManager color = FreeForAll.TeamColors.GetValueOrDefault(team, "#00ffff"); return true; case CustomGameMode.Snowdown: + case CustomGameMode.LoopWanted: case CustomGameMode.Mingle: case CustomGameMode.RoomRush: case CustomGameMode.NaturalDisasters: diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index 65cadb982..0614d544a 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -35,6 +35,7 @@ public enum CustomGameMode Deathrace = 0x0F, Mingle = 0x10, Snowdown = 0x11, + LoopWanted = 0x12, All = int.MaxValue } @@ -89,7 +90,8 @@ public static class Options "BedWars", "Deathrace", "Mingle", - "Snowdown" + "Snowdown", + "LoopWanted" ]; private static Dictionary roleCounts; @@ -928,6 +930,7 @@ public static class Options 14 => CustomGameMode.Deathrace, 15 => CustomGameMode.Mingle, 16 => CustomGameMode.Snowdown, + 17 => CustomGameMode.LoopWanted, _ => CustomGameMode.Standard }; @@ -1854,6 +1857,8 @@ public static class Options Mingle.SetupCustomOption(); // Snowdown Snowdown.SetupCustomOption(); + // LoopWanted + LoopWanted.SetupCustomOption(); yield return null; diff --git a/Modules/RPC.cs b/Modules/RPC.cs index afe51d1b4..fda9c5e26 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -179,7 +179,8 @@ public enum CustomRPC NaturalDisastersSync, TMGSync, BedWarsSync, - DeathraceSync + DeathraceSync, + LoopWantedSync // The total number of RPCs must not exceed 255 // Because HandleRpc accepts Rpc in byte (max 255) system, and it will be impossible to use int @@ -1274,6 +1275,11 @@ internal static class RPCHandlerPatch break; } + case CustomRPC.LoopWantedSync: + { + LoopWanted.ReceiveRPC(reader); + break; + } case CustomRPC.InspectorCommand: { Inspector.ReceiveRPC(reader); diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 3c5a6f709..6871d7a52 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -850,6 +850,7 @@ public static class Utils case CustomGameMode.Deathrace: case CustomGameMode.Mingle: case CustomGameMode.Snowdown: + case CustomGameMode.LoopWanted: return false; case CustomGameMode.HideAndSeek: return CustomHnS.HasTasks(p); @@ -1517,6 +1518,7 @@ public static class Utils break; case CustomGameMode.Snowdown: + case CustomGameMode.LoopWanted: case CustomGameMode.Mingle: case CustomGameMode.Deathrace: case CustomGameMode.BedWars: @@ -2545,6 +2547,7 @@ public static class Utils CustomGameMode.Deathrace => CustomRoles.Racer.ColoredTextByRole($"{modeText}\r\n") + name, CustomGameMode.Mingle => CustomRoles.MinglePlayer.ColoredTextByRole($"{modeText}\r\n") + name, CustomGameMode.Snowdown => CustomRoles.SnowdownPlayer.ColoredTextByRole($"{modeText}\r\n") + name, + CustomGameMode.LoopWanted => name, _ => name }; } @@ -3043,6 +3046,9 @@ public static class Utils case CustomGameMode.Snowdown: AdditionalSuffixes.Add(Snowdown.GetSuffix(seer, seer)); break; + case CustomGameMode.LoopWanted: + AdditionalSuffixes.Add(LoopWanted.GetSuffix(seer, seer)); + break; } for (int i = AdditionalSuffixes.Count - 1; i >= 0; i--) @@ -3110,7 +3116,7 @@ public static class Utils SelfSuffix.Append($"\n\n<#ffffff>{GetString($"GameModeTutorial.{Options.CurrentGameMode}")}\n"); } - bool noRoleText = GameStates.IsLobby || Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame or CustomGameMode.BedWars or CustomGameMode.Deathrace or CustomGameMode.Mingle or CustomGameMode.Snowdown; + bool noRoleText = GameStates.IsLobby || Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame or CustomGameMode.BedWars or CustomGameMode.Deathrace or CustomGameMode.Mingle or CustomGameMode.Snowdown or CustomGameMode.LoopWanted; // Combine the seer's job title and SelfTaskText with the seer's player name and SelfMark string selfRoleName = noRoleText ? string.Empty : $"{seer.GetDisplayRoleName()}{selfTaskText}"; @@ -3309,7 +3315,7 @@ public static class Utils if (IsRevivingRoleAlive() && Main.DiedThisRound.Contains(seer.PlayerId)) targetRoleText = string.Empty; - if (Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame or CustomGameMode.BedWars or CustomGameMode.Deathrace or CustomGameMode.Mingle or CustomGameMode.Snowdown) + if (Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame or CustomGameMode.BedWars or CustomGameMode.Deathrace or CustomGameMode.Mingle or CustomGameMode.Snowdown or CustomGameMode.LoopWanted) targetRoleText = string.Empty; if (!GameStates.IsLobby) @@ -3434,6 +3440,9 @@ public static class Utils case CustomGameMode.Snowdown: AdditionalSuffixes.Add(Snowdown.GetSuffix(seer, target)); break; + case CustomGameMode.LoopWanted when seer == target: + AdditionalSuffixes.Add(LoopWanted.GetSuffix(seer, target)); + break; } if (MeetingStates.FirstMeeting && Main.ShieldPlayer == target.FriendCode && !string.IsNullOrWhiteSpace(target.FriendCode) && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.FFA or CustomGameMode.Speedrun) @@ -3645,6 +3654,7 @@ public static class Utils case CustomGameMode.Deathrace: case CustomGameMode.Mingle: case CustomGameMode.Snowdown: + case CustomGameMode.LoopWanted: return local.Is(CustomRoles.GM); case CustomGameMode.Standard: diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index 5607e5262..c6844d6ba 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -606,6 +606,11 @@ internal static class GameEndChecker Predicate = new SnowdownGameEndPredicate(); } + public static void SetPredicateToLoopWanted() + { + Predicate = new LoopWantedGameEndPredicate(); + } + private class NormalGameEndPredicate : GameEndPredicate { public override bool CheckForGameEnd(out GameOverReason reason) @@ -1382,6 +1387,20 @@ internal static class GameEndChecker } } + private class LoopWantedGameEndPredicate : GameEndPredicate + { + public override bool CheckForGameEnd(out GameOverReason reason) + { + reason = GameOverReason.ImpostorsByKill; + return WinnerIds.Count <= 0 && CheckGameEndByLivingPlayers(out reason); + } + + private static bool CheckGameEndByLivingPlayers(out GameOverReason reason) + { + return LoopWanted.CheckForGameEnd(out reason); + } + } + public abstract class GameEndPredicate { /// Checks the game ending condition and stores the value in CustomWinnerHolder. diff --git a/Patches/GameOptionsMenuPatch.cs b/Patches/GameOptionsMenuPatch.cs index b68f79a7c..8d238a5e9 100644 --- a/Patches/GameOptionsMenuPatch.cs +++ b/Patches/GameOptionsMenuPatch.cs @@ -1216,7 +1216,8 @@ public static class GameSettingMenuPatch gms.Remove(CustomGameMode.Deathrace); } - const int totalCols = 3; + const int totalCols = 2; + int itemsPerCol = Mathf.CeilToInt(gms.Count / (float)totalCols); GMButtons.Clear(); @@ -1231,8 +1232,8 @@ public static class GameSettingMenuPatch gmButton.transform.SetParent(gameSettingsLabel.transform, false); gmButton.SetActive(true); } - gmButton.transform.localPosition = new Vector3((((index / 8) - ((totalCols - 1) / 2f)) * 1.4f) + 0.86f, gameSettingsLabelPos.y - 1.9f - (0.22f * (index % 8)), -1f); - gmButton.transform.localScale = new(0.4f, 0.3f, 1f); + gmButton.transform.localPosition = new Vector3((((index / itemsPerCol) - ((totalCols - 1) / 2f)) * 1.4f) + 0.16f, gameSettingsLabelPos.y - 1.9f - (0.17f * (index % itemsPerCol)), -1f); + gmButton.transform.localScale = new(0.35f, 0.22f, 1f); var gmButtonTmp = gmButton.transform.Find("FontPlacer/Text_TMP").GetComponent(); gmButtonTmp.alignment = TextAlignmentOptions.Center; gmButtonTmp.DestroyTranslator(); diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs index 82236fbf4..757ecc626 100644 --- a/Patches/IntroPatch.cs +++ b/Patches/IntroPatch.cs @@ -310,6 +310,16 @@ internal static class SetUpRoleTextPatch __instance.RoleBlurbText.text = GetString("SnowdownPlayerInfo"); break; } + case CustomGameMode.LoopWanted: + { + Color color = Utils.GetRoleColor(CustomRoles.LoopHunter); + __instance.YouAreText.transform.gameObject.SetActive(false); + __instance.RoleText.text = GetString("LoopHunter"); + __instance.RoleText.color = color; + __instance.RoleBlurbText.color = color; + __instance.RoleBlurbText.text = GetString("LoopHunterInfo"); + break; + } default: { CustomRoles role = lp.GetCustomRole(); @@ -1017,6 +1027,15 @@ internal static class BeginCrewmatePatch __instance.ImpostorText.text = GetString("SnowdownPlayerInfo"); break; } + case CustomGameMode.LoopWanted: + { + __instance.TeamTitle.text = GetString("LoopHunter"); + __instance.TeamTitle.color = __instance.BackgroundBar.material.color = Utils.GetRoleColor(CustomRoles.LoopHunter); + PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.Impostor); + __instance.ImpostorText.gameObject.SetActive(true); + __instance.ImpostorText.text = GetString("LoopHunterInfo"); + break; + } } return; @@ -1362,6 +1381,9 @@ internal static class IntroCutsceneDestroyPatch case CustomGameMode.Snowdown: Snowdown.GameStart(); break; + case CustomGameMode.LoopWanted: + LoopWanted.OnGameStart(); + break; } if (AFKDetector.ActivateOnStart.GetBool()) LateTask.New(() => aapc.Do(AFKDetector.RecordPosition), 1f, log: false); diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index daa8d6c1a..085943d4a 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -316,6 +316,7 @@ internal static class ChangeRoleSettings HotPotato.Init(); CustomHnS.Init(); Speedrun.Init(); + LoopWanted.Init(); } catch (Exception e) { Utils.ThrowException(e); } @@ -951,6 +952,9 @@ internal static class StartGameHostPatch case CustomGameMode.Snowdown: Snowdown.Init(); goto default; + case CustomGameMode.LoopWanted: + LoopWanted.OnGameStart(); + goto default; default: if (Options.IntegrateNaturalDisasters.GetBool()) goto case CustomGameMode.NaturalDisasters; break; @@ -1023,6 +1027,9 @@ internal static class StartGameHostPatch case CustomGameMode.Snowdown: GameEndChecker.SetPredicateToSnowdown(); break; + case CustomGameMode.LoopWanted: + GameEndChecker.SetPredicateToLoopWanted(); + break; } // Add players with unclassified roles to the list of players who require ResetCam. diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index a327b47cf..2a4258ef1 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -67,7 +67,7 @@ internal static class EndGamePatch long secondsIn = new DateTimeOffset(date.ToUniversalTime()).ToUnixTimeSeconds() - IntroCutsceneDestroyPatch.IntroDestroyTS; byte killerId = value.GetRealKiller(); bool gmIsFm = Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.StopAndGo; - bool gmIsFmhh = gmIsFm || Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.HideAndSeek or CustomGameMode.Speedrun or CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame or CustomGameMode.BedWars or CustomGameMode.Deathrace or CustomGameMode.Mingle or CustomGameMode.Snowdown; + bool gmIsFmhh = gmIsFm || Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.HideAndSeek or CustomGameMode.Speedrun or CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters or CustomGameMode.RoomRush or CustomGameMode.KingOfTheZones or CustomGameMode.Quiz or CustomGameMode.TheMindGame or CustomGameMode.BedWars or CustomGameMode.Deathrace or CustomGameMode.Mingle or CustomGameMode.Snowdown or CustomGameMode.LoopWanted; sb.Append($"\n{secondsIn / 60:00}:{secondsIn % 60:00} {Main.AllPlayerNames[key]} ({(gmIsFmhh ? string.Empty : Utils.GetDisplayRoleName(key, pure: true))}{(gmIsFm ? string.Empty : Utils.GetSubRolesText(key, summary: true))}) [{Utils.GetVitalText(key)}]"); if (killerId != byte.MaxValue && killerId != key) sb.Append($"\n\t⇐ {Main.AllPlayerNames[killerId]} ({(gmIsFmhh ? string.Empty : Utils.GetDisplayRoleName(killerId, pure: true))}{(gmIsFm ? string.Empty : Utils.GetSubRolesText(killerId, summary: true))})"); } @@ -601,6 +601,7 @@ internal static class SetEverythingUpPatch case CustomGameMode.Deathrace: case CustomGameMode.BedWars: case CustomGameMode.Quiz: + case CustomGameMode.LoopWanted: { foreach (byte id in cloneRoles.Where(EndGamePatch.SummaryText.ContainsKey)) sb.Append('\n').Append(EndGamePatch.SummaryText[id]); diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 551535f4c..48902621b 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -258,8 +258,11 @@ internal static class CheckMurderPatch case CustomGameMode.Snowdown: Snowdown.OnCheckMurder(killer, target); return false; + case CustomGameMode.LoopWanted: + LoopWanted.OnCheckMurder(killer, target); + return false; } - + PlagueBearer.CheckAndSpreadInfection(killer, target); PlagueBearer.CheckAndSpreadInfection(target, killer); @@ -2128,6 +2131,9 @@ internal static class FixedUpdatePatch case CustomGameMode.Snowdown: AdditionalSuffixes.Add(Snowdown.GetSuffix(seer, target)); break; + case CustomGameMode.LoopWanted when self: + AdditionalSuffixes.Add(LoopWanted.GetSuffix(seer, target, false)); + break; } if (MeetingStates.FirstMeeting && Main.ShieldPlayer == target.FriendCode && !string.IsNullOrWhiteSpace(target.FriendCode) && !self && Options.CurrentGameMode is CustomGameMode.Standard or CustomGameMode.SoloPVP or CustomGameMode.FFA) @@ -2309,6 +2315,7 @@ internal static class EnterVentPatch case CustomGameMode.TheMindGame: case CustomGameMode.Quiz: case CustomGameMode.SoloPVP when !SoloPVP.CanVent: + case CustomGameMode.LoopWanted: pc.MyPhysics?.RpcBootFromVent(__instance.Id); break; } diff --git a/Resources/Lang/en_US.jsonc b/Resources/Lang/en_US.jsonc index 565f10235..c0ec2ee7c 100644 --- a/Resources/Lang/en_US.jsonc +++ b/Resources/Lang/en_US.jsonc @@ -4112,6 +4112,9 @@ "SnowdownPlayer": "SNOWDOWN", "SnowdownPlayerInfo": "THROW SNOWBALLS AT PLAYERS", "SnowdownPlayerInfoLong": "THROW SNOWBALLS AT PLAYERS", + "LoopHunter": "LOOP HUNTER", + "LoopHunterInfo": "KILL YOUR ASSIGNED TARGET AND SURVIVE", + "LoopHunterInfoLong": "KILL YOUR ASSIGNED TARGET AND SURVIVE", "TMGPlayer": "THE MIND GAME", "TMGPlayerInfo": "THINK WITH OTHERS' BRAINS TO WIN", "TMGPlayerInfoLong": "THINK WITH OTHERS' BRAINS TO WIN", @@ -8188,6 +8191,25 @@ "Snowdown.SnowballGainFrequencyOption": "Initial Snowball Gaining Frequency", "Snowdown.SnowballThrowSpeedOption": "Snowball Throwing Speed", "SnowdownButtonText": "THROW", + // LoopWanted - Cycle Hunt Order + "LoopWanted": "LOOP WANTED", + "ModeLoopWanted": "Gamemode: Loop Wanted", + "ModeDescribe.LoopWanted": "In the Loop Wanted gamemode, every player has an assigned assassination target marked with an arrow. You can ONLY kill your designated target. Killing the wrong target results in a punishment (suicide / increased cooldown / lower vision). When you kill your target, you inherit their target. When only a few players remain, Carnival Mode activates — everyone gets a speed boost and reduced kill cooldown. Last player alive wins!", + "GameModeTutorial.LoopWanted": "The current game mode is <#ffb432>Loop Wanted.\\nEach player has a target marked with an arrow.\\nYou can ONLY kill your target.\\nKilling the wrong target will punish you.\\nYour target transfers when you kill correctly.\\nCarnival mode activates when few remain!", + "LoopWanted.BaseKillCooldown": "Base Kill Cooldown", + "LoopWanted.PunishmentMode": "Wrong Kill Punishment", + "LoopWanted.PunishSuicide": "Suicide", + "LoopWanted.PunishLowerVision": "Lower Vision", + "LoopWanted.LowerVisionMultiplier": "Lowered Vision Multiplier", + "LoopWanted.CarnivalPlayerCount": "Players to Trigger Carnival Mode", + "LoopWanted.CarnivalKillCooldown": "Carnival Mode Kill Cooldown", + "LoopWanted.CarnivalSpeedMultiplier": "Carnival Mode Speed Multiplier", + "LoopWanted.ShowTargetArrow": "Show Target Arrow", + "LoopWanted.CurrentTarget": "Current Target", + "LoopWanted.CarnivalModeActivated": "Carnival Mode! Speed up and kill faster!", + "LoopWanted.CarnivalModeHUD": "CARNIVAL", + "LoopWanted.WrongKillSuicide": "Wrong target! You committed suicide.", + "LoopWanted.WrongKillLowerVision": "Wrong target! Your vision has been reduced.", // SoloPVP "SoloPVP": "SOLO PVP", "ModeSoloPVP": "Gamemode: Solo PVP", diff --git a/Resources/Lang/zh_CN.jsonc b/Resources/Lang/zh_CN.jsonc index 6bbed70cb..4987712f9 100644 --- a/Resources/Lang/zh_CN.jsonc +++ b/Resources/Lang/zh_CN.jsonc @@ -4111,6 +4111,9 @@ "SnowdownPlayer": "大雪纷飞", "SnowdownPlayerInfo": "吃我一记雪球", "SnowdownPlayerInfoLong": "吃我一记雪球", + "LoopHunter": "循环追猎者", + "LoopHunterInfo": "追杀你的指定目标并活到最后", + "LoopHunterInfoLong": "追杀你的指定目标并活到最后", "TMGPlayer": "心理游戏", "TMGPlayerInfo": "THINK WITH OTHERS' BRAINS TO WIN", "TMGPlayerInfoLong": "THINK WITH OTHERS' BRAINS TO WIN", @@ -8187,6 +8190,25 @@ "Snowdown.SnowballGainFrequencyOption": "初始雪球获取频率", "Snowdown.SnowballThrowSpeedOption": "丢雪球的速度", "SnowdownButtonText": "丢", + // LoopWanted - 循环追捕令 + "LoopWanted": "循环追捕令", + "ModeLoopWanted": "游戏模式:循环追捕令", + "ModeDescribe.LoopWanted": "在循环追捕令游戏模式中,每个玩家都有一个带有箭头的追杀目标。你只能击杀你被指定的目标。击杀错误目标会受到惩罚(自杀 / 冷却增加 / 视野降低)。当你击杀目标后,目标的追杀对象将转移到你身上。当剩余人数足够少时,将进入狂欢状态——全员加速且击杀冷却减少。活到最后的人获胜!", + "GameModeTutorial.LoopWanted": "当前游戏模式为:<#ffb432>循环追捕令。\\n每个玩家都有一个带箭头的追杀目标。\\n你只能击杀你所指定目标!\\n击杀错误目标会受到惩罚。\\n击杀正确目标后目标转移。\\n剩余人数过少时将进入狂欢状态!", + "LoopWanted.BaseKillCooldown": "基础击杀冷却", + "LoopWanted.PunishmentMode": "击杀错误后的惩罚", + "LoopWanted.PunishSuicide": "自杀", + "LoopWanted.PunishLowerVision": "视野降低", + "LoopWanted.LowerVisionMultiplier": "降低视野倍率", + "LoopWanted.CarnivalPlayerCount": "进入狂欢模式的人数", + "LoopWanted.CarnivalKillCooldown": "狂欢状态击杀冷却", + "LoopWanted.CarnivalSpeedMultiplier": "狂欢状态下加速倍率", + "LoopWanted.ShowTargetArrow": "显示目标箭头", + "LoopWanted.CurrentTarget": "当前目标", + "LoopWanted.CarnivalModeActivated": "进入狂欢状态!全速前进,击杀加速!", + "LoopWanted.CarnivalModeHUD": "狂欢中", + "LoopWanted.WrongKillSuicide": "目标是错误的!你自杀了。", + "LoopWanted.WrongKillLowerVision": "目标是错误的!你的视野降低了。", // SoloPVP "SoloPVP": "角逐竞技", "ModeSoloPVP": "游戏模式:角逐竞技",