New game mode: LoopWanted (Cycle Hunt Order) (#631)

* Add new Gamemode and fix SettingMenu button order issue

* back path
This commit is contained in:
Christina authored and GitHub committed 2026-06-20 11:29:09 +02:00
1 parent 746c0f1e5d
commit dd50ae4cae
22 files changed
+605 -15

No files matched your search

+3
View File
@@ -504,6 +504,9 @@ public enum CustomRoles
// Snowdown
SnowdownPlayer,
// LoopWanted
LoopHunter,
// GM
GM,
+3
View File
@@ -504,6 +504,9 @@ public enum CustomRoles
// Snowdown
SnowdownPlayer,
// LoopWanted
LoopHunter,
// GM
GM,
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AmongUsPath>D:\SteamLibrary\steamapps\common\Among Us</AmongUsPath>
<AmongUsPath>D:\SteamLibrary\steamapps\common\Among Us</AmongUsPath>
</PropertyGroup>
<Import Project="local.props" Condition="Exists('local.props')" />
<PropertyGroup>
+442
View File
@@ -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<byte, byte> TargetMap = [];
private static Dictionary<byte, long> 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<byte> 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($"<color=#ffb432>{(hud ? GetString("LoopWanted.CurrentTarget") : GetString("Target"))}:</color> <b>{targetName.RemoveHtmlTags().Replace("\r\n", string.Empty)}</b> {arrow}");
if (CarnivalMode)
sb.Append($"\n<color=#ff6600>{GetString("LoopWanted.CarnivalModeHUD")}</color>");
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<byte>();
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<byte>();
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();
}
}
}
+4 -2
View File
@@ -147,7 +147,8 @@ public class Main : BasePlugin
[CustomGameMode.Speedrun] = [],
[CustomGameMode.CaptureTheFlag] = [],
[CustomGameMode.NaturalDisasters] = [],
[CustomGameMode.Snowdown] = []
[CustomGameMode.Snowdown] = [],
[CustomGameMode.LoopWanted] = []
};
public static Dictionary<CustomGameMode, Color> 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 += () =>
+2 -1
View File
@@ -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()
+6 -1
View File
@@ -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,
+4 -1
View File
@@ -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]
};
+3
View File
@@ -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);
@@ -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();
+1
View File
@@ -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:
+6 -1
View File
@@ -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<CustomRoles, int> 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;
+7 -1
View File
@@ -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);
+12 -2
View File
@@ -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}")}</color>\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 : $"<size={fontSize}>{seer.GetDisplayRoleName()}{selfTaskText}</size>";
@@ -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:
+19
View File
@@ -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
{
/// <summary>Checks the game ending condition and stores the value in CustomWinnerHolder. </summary>
+4 -3
View File
@@ -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<TextMeshPro>();
gmButtonTmp.alignment = TextAlignmentOptions.Center;
gmButtonTmp.DestroyTranslator();
+22
View File
@@ -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);
+7
View File
@@ -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.
+2 -1
View File
@@ -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]);
+8 -1
View File
@@ -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;
}
+22
View File
@@ -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": "<size=90%>The current game mode is <#ffb432>Loop Wanted</color>.</size>\\n<size=80%>Each 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!</size>",
"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": "<color=#ff6600>Carnival Mode! Speed up and kill faster!</color>",
"LoopWanted.CarnivalModeHUD": "CARNIVAL",
"LoopWanted.WrongKillSuicide": "<color=#ff0000>Wrong target! You committed suicide.</color>",
"LoopWanted.WrongKillLowerVision": "<color=#ff0000>Wrong target! Your vision has been reduced.</color>",
// SoloPVP
"SoloPVP": "SOLO PVP",
"ModeSoloPVP": "Gamemode: Solo PVP",
+22
View File
@@ -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": "<size=90%>当前游戏模式为:<#ffb432>循环追捕令</color>。</size>\\n<size=80%>每个玩家都有一个带箭头的追杀目标。\\n你只能击杀你所指定目标!\\n击杀错误目标会受到惩罚。\\n击杀正确目标后目标转移。\\n剩余人数过少时将进入狂欢状态!</size>",
"LoopWanted.BaseKillCooldown": "基础击杀冷却",
"LoopWanted.PunishmentMode": "击杀错误后的惩罚",
"LoopWanted.PunishSuicide": "自杀",
"LoopWanted.PunishLowerVision": "视野降低",
"LoopWanted.LowerVisionMultiplier": "降低视野倍率",
"LoopWanted.CarnivalPlayerCount": "进入狂欢模式的人数",
"LoopWanted.CarnivalKillCooldown": "狂欢状态击杀冷却",
"LoopWanted.CarnivalSpeedMultiplier": "狂欢状态下加速倍率",
"LoopWanted.ShowTargetArrow": "显示目标箭头",
"LoopWanted.CurrentTarget": "当前目标",
"LoopWanted.CarnivalModeActivated": "<color=#ff6600>进入狂欢状态!全速前进,击杀加速!</color>",
"LoopWanted.CarnivalModeHUD": "狂欢中",
"LoopWanted.WrongKillSuicide": "<color=#ff0000>目标是错误的!你自杀了。</color>",
"LoopWanted.WrongKillLowerVision": "<color=#ff0000>目标是错误的!你的视野降低了。</color>",
// SoloPVP
"SoloPVP": "角逐竞技",
"ModeSoloPVP": "游戏模式:角逐竞技",