mirror of
https://github.com/Gurge44/EndlessHostRoles.git
synced 2026-10-19 15:51:58 +00:00
1727 lines
84 KiB
C#
1727 lines
84 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using AmongUs.GameOptions;
|
|
using EHR.Gamemodes;
|
|
using EHR.Modules;
|
|
using EHR.Roles;
|
|
using HarmonyLib;
|
|
using Hazel;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using static EHR.Translator;
|
|
|
|
namespace EHR;
|
|
|
|
public static class GuessManager
|
|
{
|
|
private const int MaxOneScreenRole = 40;
|
|
private static readonly int Mask = Shader.PropertyToID("_Mask");
|
|
public static HashSet<byte> Guessers = [];
|
|
private static int Page;
|
|
private static GameObject GuesserUI;
|
|
private static Dictionary<CustomRoleTypes, List<Transform>> RoleButtons;
|
|
private static Dictionary<CustomRoleTypes, SpriteRenderer> RoleSelectButtons;
|
|
private static List<SpriteRenderer> PageButtons;
|
|
private static CustomRoleTypes CurrentTeamType;
|
|
|
|
public static TextMeshPro TextTemplate;
|
|
|
|
public static string GetFormatString()
|
|
{
|
|
return Main.EnumeratePlayerControls().Aggregate(GetString("PlayerIdList"), (current, pc) => current + $"\n{pc.PlayerId.ToString()} → {pc.GetRealName()}");
|
|
}
|
|
|
|
public static bool CheckCommand(ref string msg, string command, bool exact)
|
|
{
|
|
Utils.CheckServerCommand(ref msg, out _);
|
|
|
|
string[] comList = command.Split('|');
|
|
|
|
foreach (string str in comList)
|
|
{
|
|
if (exact)
|
|
{
|
|
if (msg == "/" + str) return true;
|
|
}
|
|
else
|
|
{
|
|
if (msg.StartsWith("/" + str))
|
|
{
|
|
msg = msg.Replace("/" + str, string.Empty);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static bool GuesserMsg(PlayerControl pc, string msg, bool isUI = false)
|
|
{
|
|
if (!AmongUsClient.Instance.AmHost) return false;
|
|
if (!GameStates.IsMeeting || MeetingHud.Instance.state is MeetingHud.MeetingStates.Results or MeetingHud.MeetingStates.Proceeding || !pc) return false;
|
|
|
|
bool hasGuessingRole = pc.GetCustomRole() is CustomRoles.NiceGuesser or CustomRoles.EvilGuesser or CustomRoles.Doomsayer or CustomRoles.Prosecutor or CustomRoles.Swapper or CustomRoles.Councillor or CustomRoles.NecroGuesser or CustomRoles.Augur;
|
|
if (!hasGuessingRole && !pc.Is(CustomRoles.Guesser) && !Options.GuesserMode.GetBool()) return false;
|
|
|
|
int operate; // 1: ID, 2: Guess
|
|
msg = msg.ToLower().TrimStart().TrimEnd();
|
|
|
|
if (CheckCommand(ref msg, "id|guesslist|gl编号|玩家编号|玩家id|id列表|玩家列表|列表|所有id|全部id", true)) operate = 1;
|
|
else if (CheckCommand(ref msg, "shoot|guess|bet|st|bt|猜|赌", false)) operate = 2;
|
|
else return false;
|
|
|
|
Logger.Msg(msg, "Msg Guesser");
|
|
Logger.Msg($"{operate}", "Operate");
|
|
|
|
switch (operate)
|
|
{
|
|
case 1:
|
|
Utils.SendMessage(GetFormatString(), pc.PlayerId);
|
|
break;
|
|
case 2:
|
|
{
|
|
if (!pc.IsAlive())
|
|
{
|
|
ShowMessage("GuessDead");
|
|
return true;
|
|
}
|
|
|
|
if (!Options.CanGuessDuringDiscussionTime.GetBool() && MeetingHud.Instance && MeetingHud.Instance.state is MeetingHud.MeetingStates.Discussion or MeetingHud.MeetingStates.Animating && Main.RealOptionsData.GetInt(Int32OptionNames.DiscussionTime) > 0)
|
|
{
|
|
ShowMessage("GuessDuringDiscussion");
|
|
return true;
|
|
}
|
|
|
|
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.Decryptor) && Decryptor.GuessMode.GetValue() == 0 ||
|
|
Options.GuesserNumRestrictions.GetBool() && !Guessers.Contains(pc.PlayerId))
|
|
{
|
|
if (pc.GetCustomRole() is CustomRoles.EvilGuesser or CustomRoles.NiceGuesser) goto SkipCheck;
|
|
if (pc.Is(CustomRoles.Guesser)) goto SkipCheck;
|
|
if (hasGuessingRole) goto SkipCheck;
|
|
if ((pc.Is(CustomRoles.Madmate) || pc.IsConverted()) && Options.BetrayalAddonsCanGuess.GetBool()) goto SkipCheck;
|
|
if (pc.Is(CustomRoles.Arsonist) && Arsonist.ArsonistCanIgniteAnytime.GetBool() && Options.NeutralKillersCanGuess.GetBool()) goto SkipCheck;
|
|
|
|
ShowMessage("GuessNotAllowed");
|
|
return true;
|
|
}
|
|
|
|
SkipCheck:
|
|
|
|
if (!MsgToPlayerAndRole(msg, out byte targetId, out CustomRoles role, out string error))
|
|
{
|
|
ShowMessage(error);
|
|
return true;
|
|
}
|
|
|
|
if (pc.IsCrewmate() && role.IsCrewmate() && !Options.CrewCanGuessCrew.GetBool() ||
|
|
pc.IsImpostor() && role.IsImpostor() && !Options.ImpCanGuessImp.GetBool())
|
|
{
|
|
ShowMessage("GuessTeamMate");
|
|
return true;
|
|
}
|
|
|
|
PlayerControl target = Utils.GetPlayerById(targetId);
|
|
|
|
if (target)
|
|
{
|
|
Main.GuesserGuessed.TryAdd(pc.PlayerId, 0);
|
|
Main.GuesserGuessedMeeting.TryAdd(pc.PlayerId, 0);
|
|
|
|
var guesserSuicide = false;
|
|
|
|
switch (pc.Is(CustomRoles.NecroGuesser))
|
|
{
|
|
case true when target.IsAlive() || target.Is(CustomRoles.Gravestone) || (Options.SeeEjectedRolesInMeeting.GetBool() || Options.CEMode.GetValue() == 2) && Main.PlayerStates[targetId].deathReason == PlayerState.DeathReason.Vote:
|
|
ShowMessage(target.IsAlive() ? "NecroGuesser.TargetAliveError" : "NecroGuesser.TargetRevealedError");
|
|
return true;
|
|
case false when !target.IsAlive():
|
|
ShowMessage("GuessNull");
|
|
return true;
|
|
}
|
|
|
|
if (!pc.Is(CustomRoles.Guesser) && CopyCat.Instances.Exists(x => x.CopyCatPC == pc))
|
|
{
|
|
ShowMessage("GuessDisabled");
|
|
return true;
|
|
}
|
|
|
|
if (CustomTeamManager.AreInSameCustomTeam(pc.PlayerId, targetId) && !CustomTeamManager.IsSettingEnabledForPlayerTeam(targetId, CTAOption.GuessEachOther))
|
|
{
|
|
ShowMessage("GuessSameCTAPlayer");
|
|
return true;
|
|
}
|
|
|
|
bool hasGuessSetting = Options.AddonGuessSettings.TryGetValue(role, out OptionItem guessSetting);
|
|
|
|
if (hasGuessSetting && guessSetting.GetValue() == 1)
|
|
{
|
|
ShowMessage("GuessDisabledAddonOverride");
|
|
return true;
|
|
}
|
|
|
|
OptionItem convertedGuessSetting = null;
|
|
|
|
if (role == CustomRoles.Egoist || role.IsConverted())
|
|
{
|
|
convertedGuessSetting = role switch
|
|
{
|
|
CustomRoles.Charmed => Options.CharmedCanBeGuessed,
|
|
CustomRoles.Contagious => Options.ContagiousCanBeGuessed,
|
|
CustomRoles.Undead => Options.UndeadCanBeGuessed,
|
|
CustomRoles.Egoist => Options.EgoistCanBeGuessed,
|
|
CustomRoles.Entranced => Options.EntrancedCanBeGuessed,
|
|
_ => null
|
|
};
|
|
|
|
if (convertedGuessSetting?.GetValue() == 1)
|
|
{
|
|
ShowMessage("GuessDisabledAddonOverride");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool forceAllowGuess = hasGuessSetting && guessSetting.GetValue() == 0 || convertedGuessSetting?.GetValue() == 0 || role is CustomRoles.LovingCrewmate or CustomRoles.LovingImpostor or CustomRoles.Lovers && Lovers.GuessAbility.GetValue() == 2;
|
|
|
|
if (role == CustomRoles.Lovers && Lovers.GuessAbility.GetValue() == 0)
|
|
{
|
|
ShowMessage("GuessLovers");
|
|
return true;
|
|
}
|
|
|
|
if (pc.GetCustomRole() is not (CustomRoles.Augur or CustomRoles.EvilGuesser or CustomRoles.NiceGuesser or CustomRoles.Doomsayer) && (Main.GuesserGuessed[pc.PlayerId] >= Options.GuesserMaxKillsPerGame.GetInt() || Main.GuesserGuessedMeeting[pc.PlayerId] >= Options.GuesserMaxKillsPerMeeting.GetInt()))
|
|
{
|
|
ShowMessage("GGGuessMax");
|
|
return true;
|
|
}
|
|
|
|
switch (pc.GetCustomRole())
|
|
{
|
|
case CustomRoles.Augur when !((Augur)Main.PlayerStates[pc.PlayerId].Role).CanGuess:
|
|
case CustomRoles.Augur when Main.GuesserGuessed[pc.PlayerId] >= Augur.MaxGuessesPerGame.GetInt():
|
|
case CustomRoles.Augur when Main.GuesserGuessedMeeting[pc.PlayerId] >= Augur.MaxGuessesPerMeeting.GetInt():
|
|
case CustomRoles.NiceGuesser when Main.GuesserGuessed[pc.PlayerId] >= Options.GGCanGuessTime.GetInt():
|
|
case CustomRoles.EvilGuesser when Main.GuesserGuessed[pc.PlayerId] >= Options.EGCanGuessTime.GetInt():
|
|
ShowMessage("GGGuessMax");
|
|
return true;
|
|
case CustomRoles.Shifter when !Shifter.CanGuess.GetBool():
|
|
case CustomRoles.Specter when !Options.PhantomCanGuess.GetBool():
|
|
case CustomRoles.Terrorist when !Terrorist.TerroristCanGuess.GetBool():
|
|
case CustomRoles.Workaholic when !Workaholic.WorkaholicCanGuess.GetBool():
|
|
case CustomRoles.God when !God.GodCanGuess.GetBool():
|
|
case CustomRoles.Revenant:
|
|
case CustomRoles.Executioner when Executioner.Target[pc.PlayerId] == target.PlayerId && Executioner.KnowTargetRole.GetBool() && !Executioner.CanGuessTarget.GetBool():
|
|
ShowMessage("GuessDisabled");
|
|
return true;
|
|
case CustomRoles.Monarch when role == CustomRoles.Knighted:
|
|
ShowMessage("GuessKnighted");
|
|
return true;
|
|
case CustomRoles.Berserker when ((Berserker)Main.PlayerStates[pc.PlayerId].Role).Form >= 4:
|
|
ShowMessage("GuessBerserker");
|
|
return true;
|
|
case CustomRoles.Doomsayer:
|
|
if (Doomsayer.CantGuess)
|
|
{
|
|
ShowMessage("DoomsayerCantGuess");
|
|
return true;
|
|
}
|
|
|
|
if ((
|
|
target.IsImpostor() && !Doomsayer.DCanGuessImpostors.GetBool() ||
|
|
target.IsCrewmate() && !Doomsayer.DCanGuessCrewmates.GetBool() ||
|
|
(role.IsNeutral() || target.IsNeutralKiller()) && !Doomsayer.DCanGuessNeutrals.GetBool() ||
|
|
role.IsCoven() && !Doomsayer.DCanGuessCoven.GetBool())
|
|
&& !forceAllowGuess)
|
|
{
|
|
ShowMessage("GuessNotAllowed");
|
|
return true;
|
|
}
|
|
|
|
if (role.IsAdditionRole() && !Doomsayer.DCanGuessAdt.GetBool() && !forceAllowGuess)
|
|
{
|
|
ShowMessage("GuessAdtRole");
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (Medic.ProtectList.Contains(target.PlayerId) && !Medic.GuesserIgnoreShield.GetBool())
|
|
{
|
|
ShowMessage("GuessShielded");
|
|
return true;
|
|
}
|
|
|
|
switch (role)
|
|
{
|
|
case CustomRoles.Crewmate or CustomRoles.CrewmateEHR when CrewmateVanillaRoles.VanillaCrewmateCannotBeGuessed.GetBool():
|
|
ShowMessage("GuessVanillaCrewmate");
|
|
return true;
|
|
case CustomRoles.Workaholic when Workaholic.WorkaholicVisibleToEveryone.GetBool():
|
|
ShowMessage("GuessWorkaholic");
|
|
return true;
|
|
case CustomRoles.Doctor when Options.DoctorVisibleToEveryone.GetBool() && !target.HasEvilAddon():
|
|
ShowMessage("GuessDoctor");
|
|
return true;
|
|
case CustomRoles.Marshall when target.Is(CustomRoles.Marshall) && !Marshall.CanBeGuessedOnTaskCompletion.GetBool() && target.GetTaskState().IsTaskFinished:
|
|
ShowMessage("GuessMarshallTask");
|
|
return true;
|
|
case CustomRoles.Monarch when pc.Is(CustomRoles.Knighted):
|
|
ShowMessage("GuessMonarch");
|
|
return true;
|
|
case CustomRoles.Mayor when target.Is(CustomRoles.Mayor) && Mayor.MayorRevealWhenDoneTasks.GetBool() && target.GetTaskState().IsTaskFinished:
|
|
ShowMessage("GuessMayor");
|
|
return true;
|
|
case CustomRoles.Bait when Options.BaitNotification.GetBool():
|
|
ShowMessage("GuessNotifiedBait");
|
|
return true;
|
|
case CustomRoles.Pestilence:
|
|
ShowMessage("GuessPestilence");
|
|
if (DoubleShot.CheckGuess(pc, isUI)) return true;
|
|
guesserSuicide = true;
|
|
break;
|
|
case CustomRoles.Specter:
|
|
ShowMessage("GuessPhantom");
|
|
return true;
|
|
case CustomRoles.Snitch when target.Is(CustomRoles.Snitch) && pc.IsSnitchTarget() && target.GetTaskState().RemainingTasksCount <= Snitch.RemainingTasksToBeFound:
|
|
ShowMessage("EGGuessSnitchTaskDone");
|
|
return true;
|
|
case CustomRoles.Merchant when Merchant.IsBribedKiller(pc, target):
|
|
ShowMessage("BribedByMerchant2");
|
|
return true;
|
|
case CustomRoles.SuperStar:
|
|
ShowMessage("GuessSuperStar");
|
|
return true;
|
|
case CustomRoles.Backstabber when target.Is(CustomRoles.Backstabber) && Backstabber.RevealAfterKilling.GetBool() && target.GetAbilityUseLimit() == 0f:
|
|
ShowMessage("GuessBackstabber");
|
|
return true;
|
|
case CustomRoles.President when Main.PlayerStates[target.PlayerId].Role is President { IsRevealed: true }:
|
|
ShowMessage("GuessPresident");
|
|
return true;
|
|
case CustomRoles.EvilEraser when EvilEraser.ErasedPlayers.Contains(target.PlayerId) && pc.Is(CustomRoles.EvilEraser):
|
|
case CustomRoles.NiceEraser when NiceEraser.ErasedPlayers.Contains(target.PlayerId) && pc.Is(CustomRoles.NiceEraser):
|
|
ShowMessage("GuessErased");
|
|
return true;
|
|
case CustomRoles.Tank when !Tank.CanBeGuessed.GetBool():
|
|
ShowMessage("GuessTank");
|
|
return true;
|
|
case CustomRoles.Ankylosaurus:
|
|
ShowMessage("GuessAnkylosaurus");
|
|
return true;
|
|
case CustomRoles.Evader:
|
|
case CustomRoles.Tree:
|
|
case CustomRoles.Car:
|
|
case CustomRoles.DonutDelivery when DonutDelivery.IsUnguessable(pc, target):
|
|
case CustomRoles.Shifter:
|
|
case CustomRoles.Speedrunner when target.Is(CustomRoles.Speedrunner) && !pc.Is(Team.Crewmate) && target.GetTaskState().CompletedTasksCount >= Speedrunner.SpeedrunnerNotifyAtXTasksLeft.GetInt() && Speedrunner.SpeedrunnerNotifyKillers.GetBool():
|
|
case CustomRoles.Goose when !Goose.CanBeGuessed.GetBool():
|
|
case CustomRoles.BananaMan:
|
|
case CustomRoles.Disco:
|
|
case CustomRoles.Flash:
|
|
case CustomRoles.Giant:
|
|
case CustomRoles.Glow:
|
|
case CustomRoles.LastImpostor:
|
|
ShowMessage("GuessObviousAddon");
|
|
return true;
|
|
case CustomRoles.GM:
|
|
Utils.SendMessage(GetString("GuessGM"), pc.PlayerId);
|
|
return true;
|
|
}
|
|
|
|
if (target.Is(CustomRoles.Onbound))
|
|
{
|
|
Onbound.NumBlocked ??= [];
|
|
|
|
if (!Onbound.NumBlocked.TryGetValue(target.PlayerId, out HashSet<byte> attempters))
|
|
Onbound.NumBlocked[target.PlayerId] = attempters = [];
|
|
|
|
if (attempters.Count < Onbound.MaxAttemptsBlocked.GetInt())
|
|
{
|
|
attempters.Add(pc.PlayerId);
|
|
ShowMessage("GuessOnbound");
|
|
|
|
if (Onbound.GuesserSuicides.GetBool())
|
|
{
|
|
if (DoubleShot.CheckGuess(pc, isUI)) return true;
|
|
guesserSuicide = true;
|
|
}
|
|
else return true;
|
|
}
|
|
else if (attempters.Contains(pc.PlayerId))
|
|
{
|
|
ShowMessage("GuessOnbound");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (Jailor.PlayerIdList.Any(x => Main.PlayerStates[x].Role is Jailor { IsEnable: true } jl && jl.JailorTarget == target.PlayerId))
|
|
{
|
|
if (!isUI) Utils.SendMessage(GetString("CantGuessJailed"), pc.PlayerId, CustomRoles.Jailor.ColoredTextByRole(GetString("JailorTitle")));
|
|
else pc.ShowPopUp($"{CustomRoles.Jailor.ColoredTextByRole(GetString("JailorTitle"))}\n{GetString("CantGuessJailed")}");
|
|
|
|
Logger.Info($"Player {pc.GetNameWithRole()} tried to guess jailed player {target.GetNameWithRole()}", "Guesser");
|
|
return true;
|
|
}
|
|
|
|
if (Jailor.PlayerIdList.Any(x => Main.PlayerStates[x].Role is Jailor { IsEnable: true } jl && jl.JailorTarget == pc.PlayerId && role != CustomRoles.Jailor))
|
|
{
|
|
if (!isUI) Utils.SendMessage(GetString("JailedCanOnlyGuessJailor"), pc.PlayerId, CustomRoles.Jailor.ColoredTextByRole(GetString("JailorTitle")));
|
|
else pc.ShowPopUp($"{CustomRoles.Jailor.ColoredTextByRole(GetString("JailorTitle"))}\n{GetString("JailedCanOnlyGuessJailor")}");
|
|
|
|
Logger.Info($"Player {pc.GetNameWithRole()} tried to guess {target.GetNameWithRole()} while jailed", "Guesser");
|
|
return true;
|
|
}
|
|
|
|
if (Markseeker.PlayerIdList != null && Markseeker.PlayerIdList.Any(x => Main.PlayerStates[x].Role is Markseeker { TargetRevealed: true } ms && ms.MarkedId == target.PlayerId))
|
|
{
|
|
ShowMessage("GuessMarkseekerTarget");
|
|
return true;
|
|
}
|
|
|
|
// Check whether Add-on guessing is allowed
|
|
if (!forceAllowGuess && role.IsAdditionRole())
|
|
{
|
|
switch (pc.GetCustomRole())
|
|
{
|
|
// Evil & Nice Guessers Can't Guess Addons
|
|
case CustomRoles.EvilGuesser when !Options.EGCanGuessAdt.GetBool():
|
|
case CustomRoles.NiceGuesser when !Options.GGCanGuessAdt.GetBool():
|
|
ShowMessage("GuessAdtRole");
|
|
return true;
|
|
// Guesser (Add-on) Can't Guess Add-ons
|
|
default:
|
|
if (pc.Is(CustomRoles.Guesser) && !Guesser.GCanGuessAdt.GetBool())
|
|
{
|
|
ShowMessage("GuessAdtRole");
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// Guesser Mode Can/Can't Guess Addons
|
|
if (Options.GuesserMode.GetBool() && !Options.CanGuessAddons.GetBool())
|
|
{
|
|
if (Options.ImpostorsCanGuess.GetBool() && pc.Is(CustomRoleTypes.Impostor) && !(pc.GetCustomRole() == CustomRoles.EvilGuesser || pc.Is(CustomRoles.Guesser)) ||
|
|
Options.CrewmatesCanGuess.GetBool() && pc.Is(CustomRoleTypes.Crewmate) && !(pc.GetCustomRole() == CustomRoles.NiceGuesser || pc.Is(CustomRoles.Guesser)) ||
|
|
Options.CovenCanGuess.GetBool() && pc.Is(CustomRoleTypes.Coven) && !(pc.GetCustomRole() == CustomRoles.Augur || pc.Is(CustomRoles.Guesser)) ||
|
|
(Options.NeutralKillersCanGuess.GetBool() || Options.PassiveNeutralsCanGuess.GetBool()) && pc.Is(CustomRoleTypes.Neutral) && !(pc.GetCustomRole() is CustomRoles.Ritualist or CustomRoles.Doomsayer || pc.Is(CustomRoles.Guesser)))
|
|
{
|
|
ShowMessage("GuessAdtRole");
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pc.PlayerId == target.PlayerId)
|
|
{
|
|
if (!isUI) Utils.SendMessage(GetString("LaughToWhoGuessSelf"), pc.PlayerId, Utils.ColorString(Color.cyan, GetString("MessageFromKPD")));
|
|
else pc.ShowPopUp($"{Utils.ColorString(Color.cyan, GetString("MessageFromKPD"))}\n{GetString("LaughToWhoGuessSelf")}");
|
|
|
|
if (DoubleShot.CheckGuess(pc, isUI)) return true;
|
|
guesserSuicide = true;
|
|
}
|
|
else if (pc.Is(CustomRoles.NiceGuesser) && role.IsCrewmate() && !Options.GGCanGuessCrew.GetBool() && !pc.Is(CustomRoles.Madmate))
|
|
{
|
|
if (!isUI) Utils.SendMessage(GetString("GuessCrewRole"), pc.PlayerId, Utils.ColorString(Color.cyan, GetString("MessageFromGurge44")));
|
|
else pc.ShowPopUp($"{Utils.ColorString(Color.cyan, GetString("MessageFromGurge44"))}\n{GetString("GuessCrewRole")}");
|
|
|
|
return true;
|
|
}
|
|
else if (pc.Is(CustomRoles.EvilGuesser) && role.IsImpostor() && !Options.EGCanGuessImp.GetBool())
|
|
{
|
|
if (!isUI) Utils.SendMessage(GetString("GuessImpRole"), pc.PlayerId, Utils.ColorString(Color.cyan, GetString("MessageFromGurge44")));
|
|
else pc.ShowPopUp($"{Utils.ColorString(Color.cyan, GetString("MessageFromGurge44"))}\n{GetString("GuessImpRole")}");
|
|
|
|
return true;
|
|
}
|
|
else if (!target.Is(role) && !target.Is(CustomRoles.Unbound))
|
|
{
|
|
if (DoubleShot.CheckGuess(pc, isUI)) return true;
|
|
|
|
guesserSuicide = true;
|
|
Logger.Msg($"{guesserSuicide}", "guesserSuicide3");
|
|
}
|
|
|
|
Main.GuesserGuessed[pc.PlayerId]++;
|
|
Main.GuesserGuessedMeeting[pc.PlayerId]++;
|
|
|
|
if (guesserSuicide && Options.GuesserDoesntDieOnMisguess.GetBool())
|
|
{
|
|
if (!isUI) Utils.SendMessage(GetString("MisguessButNoSuicide"), pc.PlayerId, Utils.ColorString(Color.yellow, GetString("MessageFromGurge44")));
|
|
else pc.ShowPopUp($"{Utils.ColorString(Color.yellow, GetString("MessageFromGurge44"))}\n{GetString("MisguessButNoSuicide")}");
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
Logger.Info($"{pc.GetNameWithRole()} guessed {target.GetNameWithRole()}", "Guesser");
|
|
PlayerControl dp = guesserSuicide ? pc : target;
|
|
target = dp;
|
|
|
|
Logger.Info($"Player: {target.GetRealName().RemoveHtmlTags()} was guessed by {pc.GetRealName().RemoveHtmlTags()}", "Guesser");
|
|
|
|
if (pc.IsHost()) Utils.FlashColor(guesserSuicide ? new(1f, 0f, 0f, 0.3f) : new(0f, 1f, 0f, 0.3f));
|
|
|
|
if (Main.PlayerStates[pc.PlayerId].Role is NecroGuesser ng)
|
|
{
|
|
if (!guesserSuicide) ng.GuessedPlayers++;
|
|
ShowMessage(!guesserSuicide ? "NecroGuesser.GuessCorrect" : "NecroGuesser.GuessIncorrect");
|
|
return true;
|
|
}
|
|
|
|
if (pc.Is(CustomRoles.Doomsayer) && Doomsayer.AdvancedSettings.GetBool())
|
|
{
|
|
if (Doomsayer.GuessesCountPerMeeting >= Doomsayer.MaxNumberOfGuessesPerMeeting.GetInt() && pc.PlayerId != dp.PlayerId)
|
|
{
|
|
ShowMessage("DoomsayerCantGuess");
|
|
return true;
|
|
}
|
|
|
|
Doomsayer.GuessesCountPerMeeting++;
|
|
|
|
if (Doomsayer.GuessesCountPerMeeting >= Doomsayer.MaxNumberOfGuessesPerMeeting.GetInt())
|
|
Doomsayer.CantGuess = true;
|
|
|
|
if (!Doomsayer.KillCorrectlyGuessedPlayers.GetBool() && pc.PlayerId != dp.PlayerId)
|
|
{
|
|
ShowMessage("DoomsayerCorrectlyGuessRole");
|
|
|
|
if (Doomsayer.GuessedRoles.Contains(role))
|
|
LateTask.New(() => Utils.SendMessage(GetString("DoomsayerGuessSameRoleAgainMsg"), pc.PlayerId, CustomRoles.Doomsayer.ColoredTextByRole(GetString("DoomsayerGuessCountTitle"))), 0.7f, "Doomsayer Guess Same Role Again Msg");
|
|
else
|
|
{
|
|
Doomsayer.GuessingToWin[pc.PlayerId]++;
|
|
Doomsayer.SendRPC(pc);
|
|
Doomsayer.GuessedRoles.Add(role);
|
|
|
|
LateTask.New(() => Utils.SendMessage(string.Format(GetString("DoomsayerGuessCountMsg"), Doomsayer.GuessingToWin[pc.PlayerId]), pc.PlayerId, CustomRoles.Doomsayer.ColoredTextByRole(GetString("DoomsayerGuessCountTitle"))), 0.7f, "Doomsayer Guess Msg 1");
|
|
}
|
|
|
|
Doomsayer.CheckCountGuess(pc);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (Doomsayer.DoesNotSuicideWhenMisguessing.GetBool() && pc.PlayerId == dp.PlayerId)
|
|
{
|
|
ShowMessage("DoomsayerNotCorrectlyGuessRole");
|
|
|
|
if (Doomsayer.MisguessRolePrevGuessRoleUntilNextMeeting.GetBool())
|
|
Doomsayer.CantGuess = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (pc.Is(CustomRoles.Augur) && pc.PlayerId == dp.PlayerId)
|
|
{
|
|
ShowMessage("Augur.IncorrectGuess");
|
|
((Augur)Main.PlayerStates[pc.PlayerId].Role).CanGuess = false;
|
|
return true;
|
|
}
|
|
|
|
string name = dp.GetRealName();
|
|
if (!Options.DisableKillAnimationOnGuess.GetBool()) CustomSoundsManager.RPCPlayCustomSoundAll("Gunfire");
|
|
|
|
LateTask.New(() =>
|
|
{
|
|
if (Main.PlayerStates.TryGetValue(dp.PlayerId, out PlayerState state))
|
|
{
|
|
state.deathReason = dp.PlayerId == pc.PlayerId && Options.MisguessDeathReason.GetBool() ? PlayerState.DeathReason.Misguess : PlayerState.DeathReason.Gambled;
|
|
dp.SetRealKiller(pc);
|
|
dp.RpcGuesserMurderPlayer();
|
|
}
|
|
|
|
if (dp.Is(CustomRoles.Medic)) Medic.IsDead(dp);
|
|
|
|
if (pc.Is(CustomRoles.Doomsayer) && pc.PlayerId != dp.PlayerId)
|
|
{
|
|
Doomsayer.GuessingToWin[pc.PlayerId]++;
|
|
Doomsayer.SendRPC(pc);
|
|
|
|
if (!Doomsayer.GuessedRoles.Contains(role))
|
|
Doomsayer.GuessedRoles.Add(role);
|
|
|
|
Doomsayer.CheckCountGuess(pc);
|
|
}
|
|
|
|
MeetingManager.OnGuess(dp, pc);
|
|
Utils.AfterPlayerDeathTasks(dp, true);
|
|
|
|
if (pc.AmOwner && pc.Is(CustomRoles.Decryptor) && dp.Is(CustomRoles.God))
|
|
Achievements.Type.Easypeasy.Complete();
|
|
|
|
LateTask.New(() => Utils.SendMessage(string.Format(GetString("GuessKill"), Main.AllPlayerNames.GetValueOrDefault(dp.PlayerId, name)), 255, CustomRoles.NiceGuesser.ColoredTextByRole(GetString("GuessKillTitle")), importance: MessageImportance.High), 0.6f, "Guess Msg");
|
|
|
|
if (pc.Is(CustomRoles.Doomsayer) && pc.PlayerId != dp.PlayerId) LateTask.New(() => Utils.SendMessage(string.Format(GetString("DoomsayerGuessCountMsg"), Doomsayer.GuessingToWin[pc.PlayerId]), pc.PlayerId, CustomRoles.Doomsayer.ColoredTextByRole(GetString("DoomsayerGuessCountTitle"))), 0.7f, "Doomsayer Guess Msg 2");
|
|
if (pc.Is(CustomRoles.Stealer) && pc.PlayerId != dp.PlayerId) LateTask.New(() => Utils.SendMessage(string.Format(GetString("StealerGetVote"), (int)(Main.EnumeratePlayerControls().Count(x => x.GetRealKiller()?.PlayerId == pc.PlayerId) * Options.VotesPerKill.GetFloat())), pc.PlayerId), 0.7f, log: false);
|
|
if (pc.Is(CustomRoles.Pickpocket) && pc.PlayerId != dp.PlayerId) LateTask.New(() => Utils.SendMessage(string.Format(GetString("PickpocketGetVote"), (int)(Main.EnumeratePlayerControls().Count(x => x.GetRealKiller()?.PlayerId == pc.PlayerId) * Pickpocket.VotesPerKill.GetFloat())), pc.PlayerId), 0.7f, log: false);
|
|
}, 0.2f, "Guesser Kill");
|
|
|
|
if (guesserSuicide && pc.AmOwner)
|
|
Achievements.Type.BadLuckOrBadObservation.Complete();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
void ShowMessage(string str)
|
|
{
|
|
string text = GetString(str);
|
|
|
|
if (!isUI) Utils.SendMessage(text, pc.PlayerId);
|
|
else pc.ShowPopUp(text);
|
|
|
|
Logger.Info($"Shown/Sent: {text}", "Guesser Message");
|
|
}
|
|
}
|
|
|
|
public static void RpcGuesserMurderPlayer(this PlayerControl pc /*, float delay = 0f*/)
|
|
{
|
|
// DEATH STUFF //
|
|
try
|
|
{
|
|
GameEndChecker.ShouldNotCheck = true;
|
|
LateTask.New(() => GameEndChecker.ShouldNotCheck = false, 0.2f);
|
|
|
|
Main.PlayerStates[pc.PlayerId].SetDead();
|
|
pc.RpcExileV2();
|
|
pc.Data.IsDead = true;
|
|
|
|
var meetingHud = MeetingHud.Instance;
|
|
ProcessGuess(pc, meetingHud);
|
|
|
|
foreach (PlayerVoteArea playerVoteArea in meetingHud.playerStates)
|
|
{
|
|
if (playerVoteArea.VotedForId != pc.PlayerId) continue;
|
|
|
|
playerVoteArea.UnsetVote();
|
|
meetingHud.SetDirtyBit(1U);
|
|
|
|
PlayerControl voteAreaPlayer = Utils.GetPlayerById(playerVoteArea.PlayerId);
|
|
if (!voteAreaPlayer) continue;
|
|
|
|
if (!voteAreaPlayer.AmOwner)
|
|
{
|
|
meetingHud.RpcClearVote(playerVoteArea.PlayerId);
|
|
meetingHud.SetDirtyBit(1U);
|
|
}
|
|
else
|
|
meetingHud.ClearVote(playerVoteArea.PlayerId, true);
|
|
}
|
|
|
|
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.GuessKill, SendOption.Reliable);
|
|
writer.Write(pc.PlayerId);
|
|
AmongUsClient.Instance.FinishRpcImmediately(writer);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Utils.ThrowException(ex);
|
|
}
|
|
}
|
|
|
|
private static void ProcessGuess(PlayerControl pc, MeetingHud meetingHud)
|
|
{
|
|
if (!HudManager.InstanceExists) return;
|
|
HudManager hudManager = HudManager.Instance;
|
|
SoundManager.Instance.PlaySound(pc.KillSfx, false, 0.8f);
|
|
if (!Options.DisableKillAnimationOnGuess.GetBool()) hudManager.KillOverlay.ShowKillAnimation(pc.Data, pc.Data);
|
|
|
|
if (pc.AmOwner)
|
|
{
|
|
hudManager.ShadowQuad.gameObject.SetActive(false);
|
|
pc.cosmetics.nameText.GetComponent<MeshRenderer>().material.SetInt(Mask, 0);
|
|
pc.RpcSetScanner(false);
|
|
var importantTextTask = new GameObject("_Player").AddComponent<ImportantTextTask>();
|
|
importantTextTask.transform.SetParent(AmongUsClient.Instance.transform, false);
|
|
meetingHud.SetForegroundForDead();
|
|
}
|
|
|
|
PlayerVoteArea voteArea = meetingHud.playerStates.First(x => x.PlayerId == pc.PlayerId);
|
|
|
|
if (voteArea.DidVote)
|
|
{
|
|
voteArea.UnsetVote();
|
|
|
|
if (AmongUsClient.Instance.AmHost)
|
|
{
|
|
meetingHud.SetDirtyBit(1U);
|
|
meetingHud.RpcClearVote(voteArea.PlayerId);
|
|
meetingHud.SetDirtyBit(1U);
|
|
}
|
|
}
|
|
|
|
voteArea.AmDead = true;
|
|
voteArea.Overlay.gameObject.SetActive(true);
|
|
voteArea.Overlay.color = Color.white;
|
|
voteArea.XMark.gameObject.SetActive(true);
|
|
voteArea.XMark.transform.localScale = Vector3.one;
|
|
}
|
|
|
|
public static void RpcClientGuess(PlayerControl pc)
|
|
{
|
|
try
|
|
{
|
|
var meetingHud = MeetingHud.Instance;
|
|
ProcessGuess(pc, meetingHud);
|
|
|
|
foreach (PlayerVoteArea playerVoteArea in meetingHud.playerStates)
|
|
{
|
|
if (playerVoteArea.VotedForId != pc.PlayerId) continue;
|
|
|
|
playerVoteArea.UnsetVote();
|
|
|
|
PlayerControl voteAreaPlayer = Utils.GetPlayerById(playerVoteArea.PlayerId);
|
|
meetingHud.ClearVote(playerVoteArea.PlayerId, voteAreaPlayer.AmOwner);
|
|
}
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
}
|
|
|
|
public static bool MsgToPlayerAndRole(string msg, out byte id, out CustomRoles role, out string error)
|
|
{
|
|
if (msg.StartsWith("/")) msg = msg.Replace("/", string.Empty);
|
|
|
|
Regex r = new("\\d+");
|
|
MatchCollection mc = r.Matches(msg);
|
|
var result = string.Empty;
|
|
for (var i = 0; i < mc.Count; i++) result += mc[i]; // The matching result is a complete number, and no splicing is required here.
|
|
|
|
if (byte.TryParse(result, out byte num))
|
|
id = num;
|
|
else
|
|
{
|
|
id = byte.MaxValue;
|
|
error = GetString("GuessHelp");
|
|
role = new();
|
|
return false;
|
|
}
|
|
|
|
// Determine whether the selected player is reasonable
|
|
PlayerControl target = Utils.GetPlayerById(id);
|
|
|
|
if (!target)
|
|
{
|
|
error = GetString("GuessNull");
|
|
role = new();
|
|
return false;
|
|
}
|
|
|
|
if (!ChatCommands.GetRoleByName(msg, out role))
|
|
{
|
|
error = GetString("GuessHelp");
|
|
return false;
|
|
}
|
|
|
|
error = string.Empty;
|
|
return true;
|
|
}
|
|
|
|
private static void CreateGuesserButton(MeetingHud __instance)
|
|
{
|
|
foreach (PlayerVoteArea pva in __instance.playerStates)
|
|
{
|
|
PlayerControl pc = Utils.GetPlayerById(pva.PlayerId);
|
|
|
|
if (!pc) continue;
|
|
|
|
bool skip = PlayerControl.LocalPlayer.Is(CustomRoles.NecroGuesser) switch
|
|
{
|
|
true => pc.IsAlive() || pc.Is(CustomRoles.Gravestone) || Options.SeeEjectedRolesInMeeting.GetBool() && Main.PlayerStates[pva.PlayerId].deathReason == PlayerState.DeathReason.Vote,
|
|
false => !pc.IsAlive()
|
|
};
|
|
|
|
if (skip) continue;
|
|
|
|
GameObject template = pva.Buttons.transform.Find("CancelButton").gameObject;
|
|
GameObject targetBox = Object.Instantiate(template, pva.transform);
|
|
targetBox.name = "ShootButton";
|
|
targetBox.transform.localPosition = new(-0.95f, 0.03f, -1.31f);
|
|
var renderer = targetBox.GetComponent<SpriteRenderer>();
|
|
renderer.sprite = Utils.LoadSprite("EHR.Resources.Images.Skills.TargetIcon.png", 150f);
|
|
var button = targetBox.GetComponent<PassiveButton>();
|
|
button.OnClick.RemoveAllListeners();
|
|
PlayerVoteArea pva1 = pva;
|
|
button.OnClick.AddListener(() => GuesserOnClick(pva1.PlayerId, __instance));
|
|
}
|
|
}
|
|
|
|
private static void GuesserSelectRole(CustomRoleTypes role, bool setPage = true)
|
|
{
|
|
CurrentTeamType = role;
|
|
if (setPage) Page = 1;
|
|
|
|
foreach (KeyValuePair<CustomRoleTypes, List<Transform>> roleButton in RoleButtons)
|
|
{
|
|
var index = 0;
|
|
|
|
foreach (Transform roleBtn in roleButton.Value)
|
|
{
|
|
if (!roleBtn) continue;
|
|
index++;
|
|
|
|
if (index <= (Page - 1) * 40 || Page * 40 < index)
|
|
{
|
|
roleBtn.gameObject.SetActive(false);
|
|
continue;
|
|
}
|
|
|
|
roleBtn.gameObject.SetActive(roleButton.Key == role);
|
|
}
|
|
}
|
|
|
|
foreach (KeyValuePair<CustomRoleTypes, SpriteRenderer> roleButton in RoleSelectButtons)
|
|
{
|
|
if (!roleButton.Value) continue;
|
|
roleButton.Value.color = new(0, 0, 0, roleButton.Key == role ? 1 : 0.25f);
|
|
}
|
|
}
|
|
|
|
public static void GuesserOnClick(byte playerId, MeetingHud __instance, bool specificRoleChoosing = false)
|
|
{
|
|
PlayerControl pc = Utils.GetPlayerById(playerId);
|
|
if (!pc || !pc.IsAlive() || GuesserUI || MeetingHud.Instance.state is MeetingHud.MeetingStates.Results or MeetingHud.MeetingStates.Proceeding || Starspawn.IsDayBreak) return;
|
|
|
|
try
|
|
{
|
|
Page = 1;
|
|
RoleButtons = [];
|
|
RoleSelectButtons = [];
|
|
PageButtons = [];
|
|
__instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(false));
|
|
|
|
Transform container = Object.Instantiate(GameObject.Find("PhoneUI").transform, __instance.transform);
|
|
container.transform.localPosition = new Vector3(0, 0, -200f);
|
|
GuesserUI = container.gameObject;
|
|
|
|
List<int> i = [0, 0, 0, 0, 0];
|
|
Transform buttonTemplate = __instance.playerStates[0].transform.Find("votePlayerBase");
|
|
Transform maskTemplate = __instance.playerStates[0].transform.Find("MaskArea");
|
|
Transform smallButtonTemplate = __instance.playerStates[0].Buttons.transform.Find("CancelButton");
|
|
TextTemplate.enabled = true;
|
|
Transform roleTextMeeting = TextTemplate.transform.Find("RoleTextMeeting");
|
|
if (roleTextMeeting) Object.Destroy(roleTextMeeting.gameObject);
|
|
Transform deathReasonTextMeeting = TextTemplate.transform.Find("DeathReasonTextMeeting");
|
|
if (deathReasonTextMeeting) Object.Destroy(deathReasonTextMeeting.gameObject);
|
|
|
|
Transform exitButtonParent = new GameObject().transform;
|
|
exitButtonParent.SetParent(container);
|
|
Transform exitButton = Object.Instantiate(buttonTemplate, exitButtonParent);
|
|
exitButton.Find("ControllerHighlight").gameObject.SetActive(false);
|
|
Transform exitButtonMask = Object.Instantiate(maskTemplate, exitButtonParent);
|
|
exitButtonMask.transform.localScale = new Vector3(2.88f, 0.8f, 1f);
|
|
exitButtonMask.transform.localPosition = new Vector3(0f, 0f, 1f);
|
|
exitButton.gameObject.GetComponent<SpriteRenderer>().sprite = smallButtonTemplate.GetComponent<SpriteRenderer>().sprite;
|
|
exitButtonParent.transform.localPosition = new Vector3(3.88f, 2.12f, -200f);
|
|
exitButtonParent.transform.localScale = new Vector3(0.22f, 0.9f, 1f);
|
|
exitButtonParent.transform.SetAsFirstSibling();
|
|
var passiveButton = exitButton.GetComponent<PassiveButton>();
|
|
passiveButton.OnClick.RemoveAllListeners();
|
|
passiveButton.OnClick.AddListener(() =>
|
|
{
|
|
__instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(true));
|
|
Object.Destroy(container.gameObject);
|
|
});
|
|
|
|
|
|
List<Transform> buttons = [];
|
|
Transform selectedButton = null;
|
|
|
|
var tabCount = 0;
|
|
|
|
for (var index = 0; index < 5; index++)
|
|
{
|
|
switch (PlayerControl.LocalPlayer.GetCustomRole(), index)
|
|
{
|
|
case (CustomRoles.EvilGuesser, 1) when !Options.EGCanGuessImp.GetBool():
|
|
case (CustomRoles.EvilGuesser, 4) when !Options.EGCanGuessAdt.GetBool():
|
|
case (CustomRoles.NiceGuesser, 0) when !Options.GGCanGuessCrew.GetBool() && !PlayerControl.LocalPlayer.IsMadmate():
|
|
case (CustomRoles.NiceGuesser, 4) when !Options.GGCanGuessAdt.GetBool():
|
|
case (CustomRoles.Loner, 0) when specificRoleChoosing:
|
|
case (CustomRoles.Loner, 2) when specificRoleChoosing:
|
|
case (CustomRoles.Loner, 3) when specificRoleChoosing:
|
|
case (CustomRoles.Loner, 4) when specificRoleChoosing:
|
|
case (CustomRoles.Changeling, 0) when specificRoleChoosing:
|
|
case (CustomRoles.Changeling, 2) when specificRoleChoosing:
|
|
case (CustomRoles.Changeling, 3) when specificRoleChoosing:
|
|
case (CustomRoles.Changeling, 4) when specificRoleChoosing:
|
|
case (CustomRoles.Inquirer, 4) when specificRoleChoosing:
|
|
case (CustomRoles.Forger, 4) when specificRoleChoosing:
|
|
case (CustomRoles.Pawn, 4) when specificRoleChoosing:
|
|
continue;
|
|
}
|
|
|
|
Transform teambuttonParent = new GameObject().transform;
|
|
teambuttonParent.SetParent(container);
|
|
Transform teambutton = Object.Instantiate(buttonTemplate, teambuttonParent);
|
|
teambutton.Find("ControllerHighlight").gameObject.SetActive(false);
|
|
Object.Instantiate(maskTemplate, teambuttonParent);
|
|
TextMeshPro teamlabel = Object.Instantiate(TextTemplate, teambutton);
|
|
var spriteRenderer = teambutton.GetComponent<SpriteRenderer>();
|
|
spriteRenderer.sprite = CustomButton.Get("GuessPlate");
|
|
RoleSelectButtons.Add((CustomRoleTypes)index, spriteRenderer);
|
|
teambuttonParent.localPosition = new(-3.10f + tabCount++ * 1.47f, 2.225f, -200);
|
|
teambuttonParent.localScale = new(0.53f, 0.53f, 1f);
|
|
|
|
teamlabel.color = (CustomRoleTypes)index switch
|
|
{
|
|
CustomRoleTypes.Coven => Team.Coven.GetColor(),
|
|
CustomRoleTypes.Crewmate => new Color32(140, 255, 255, byte.MaxValue),
|
|
CustomRoleTypes.Impostor => new Color32(255, 25, 25, byte.MaxValue),
|
|
CustomRoleTypes.Neutral => new Color32(255, 171, 27, byte.MaxValue),
|
|
CustomRoleTypes.Addon => new Color32(255, 154, 206, byte.MaxValue),
|
|
_ => throw new ArgumentOutOfRangeException("The index is out of range, it's an invalid CustomRoleTypes (GuessManager.cs:GuesserOnClick method)", innerException: null)
|
|
};
|
|
|
|
Logger.Info(teamlabel.color.ToString(), ((CustomRoleTypes)index).ToString());
|
|
teamlabel.text = GetString("Type" + (CustomRoleTypes)index);
|
|
teamlabel.alignment = TextAlignmentOptions.Center;
|
|
teamlabel.transform.localPosition = new Vector3(0, 0, teamlabel.transform.localPosition.z);
|
|
teamlabel.transform.localScale *= 1.6f;
|
|
teamlabel.autoSizeTextContainer = true;
|
|
|
|
if (PlayerControl.LocalPlayer.IsAlive()) CreateTeamButton(teambutton, (CustomRoleTypes)index);
|
|
continue;
|
|
|
|
static void CreateTeamButton(Transform teambutton, CustomRoleTypes type)
|
|
{
|
|
var passiveButton = teambutton.GetComponent<PassiveButton>();
|
|
passiveButton.OnClick.RemoveAllListeners();
|
|
passiveButton.OnClick.AddListener(() =>
|
|
{
|
|
GuesserSelectRole(type);
|
|
ReloadPage();
|
|
});
|
|
}
|
|
}
|
|
|
|
static void ReloadPage()
|
|
{
|
|
PageButtons[0].color = new(1, 1, 1, 1f);
|
|
PageButtons[1].color = new(1, 1, 1, 1f);
|
|
|
|
if (RoleButtons.TryGetValue(CurrentTeamType, out List<Transform> roleButtons))
|
|
{
|
|
if (roleButtons.Count / MaxOneScreenRole + (roleButtons.Count % MaxOneScreenRole != 0 ? 1 : 0) < Page)
|
|
{
|
|
Page -= 1;
|
|
PageButtons[1].color = new(1, 1, 1, 0.1f);
|
|
}
|
|
else if (roleButtons.Count / MaxOneScreenRole + (roleButtons.Count % MaxOneScreenRole != 0 ? 1 : 0) < Page + 1)
|
|
PageButtons[1].color = new(1, 1, 1, 0.1f);
|
|
}
|
|
|
|
if (Page <= 1)
|
|
{
|
|
Page = 1;
|
|
PageButtons[0].color = new(1, 1, 1, 0.1f);
|
|
}
|
|
|
|
GuesserSelectRole(CurrentTeamType, false);
|
|
}
|
|
|
|
static void CreatePage(bool isNext, MeetingHud __instance, Transform container)
|
|
{
|
|
Transform buttonTemplate = __instance.playerStates[0].transform.Find("votePlayerBase");
|
|
Transform maskTemplate = __instance.playerStates[0].transform.Find("MaskArea");
|
|
Transform pagebuttonParent = new GameObject().transform;
|
|
pagebuttonParent.SetParent(container);
|
|
Transform pagebutton = Object.Instantiate(buttonTemplate, pagebuttonParent);
|
|
pagebutton.Find("ControllerHighlight").gameObject.SetActive(false);
|
|
Object.Instantiate(maskTemplate, pagebuttonParent);
|
|
TextMeshPro pagelabel = Object.Instantiate(TextTemplate, pagebutton);
|
|
pagebutton.GetComponent<SpriteRenderer>().sprite = CustomButton.Get("GuessPlateKPD");
|
|
pagebuttonParent.localPosition = isNext ? new(3.535f, -2.2f, -200) : new(-3.475f, -2.2f, -200);
|
|
pagebuttonParent.localScale = new(0.55f, 0.55f, 1f);
|
|
pagelabel.color = Color.white;
|
|
pagelabel.text = GetString(isNext ? "NextPage" : "PreviousPage");
|
|
pagelabel.alignment = TextAlignmentOptions.Center;
|
|
pagelabel.transform.localPosition = new Vector3(0, 0, pagelabel.transform.localPosition.z);
|
|
pagelabel.transform.localScale *= 1.6f;
|
|
pagelabel.autoSizeTextContainer = true;
|
|
if (!isNext && Page <= 1) pagebutton.GetComponent<SpriteRenderer>().color = new(1, 1, 1, 0.1f);
|
|
var passiveButton = pagebutton.GetComponent<PassiveButton>();
|
|
passiveButton.OnClick.RemoveAllListeners();
|
|
passiveButton.OnClick.AddListener(ClickEvent);
|
|
PageButtons.Add(pagebutton.GetComponent<SpriteRenderer>());
|
|
return;
|
|
|
|
void ClickEvent()
|
|
{
|
|
if (isNext) Page += 1;
|
|
else Page -= 1;
|
|
|
|
if (Page < 1) Page = 1;
|
|
ReloadPage();
|
|
}
|
|
}
|
|
|
|
if (PlayerControl.LocalPlayer.IsAlive())
|
|
{
|
|
CreatePage(false, __instance, container);
|
|
CreatePage(true, __instance, container);
|
|
}
|
|
|
|
foreach (CustomRoles role in Main.CustomRoleValues)
|
|
{
|
|
if (!ShowRoleOnUI(role, specificRoleChoosing)) continue;
|
|
|
|
CreateRole(role);
|
|
}
|
|
|
|
void CreateRole(CustomRoles role)
|
|
{
|
|
CustomRoleTypes customRoleTypes = role.GetCustomRoleTypes();
|
|
var customRoleTypesInt = (int)customRoleTypes;
|
|
if (40 <= i[customRoleTypesInt]) i[customRoleTypesInt] = 0;
|
|
Transform buttonParent = new GameObject().transform;
|
|
buttonParent.SetParent(container);
|
|
Transform button = Object.Instantiate(buttonTemplate, buttonParent);
|
|
button.Find("ControllerHighlight").gameObject.SetActive(false);
|
|
Object.Instantiate(maskTemplate, buttonParent);
|
|
TextMeshPro label = Object.Instantiate(TextTemplate, button);
|
|
|
|
button.GetComponent<SpriteRenderer>().sprite = CustomButton.Get("GuessPlate");
|
|
|
|
if (!RoleButtons.ContainsKey(customRoleTypes))
|
|
RoleButtons.Add(customRoleTypes, []);
|
|
|
|
RoleButtons[customRoleTypes].Add(button);
|
|
buttons.Add(button);
|
|
int row = i[customRoleTypesInt] / 5;
|
|
int col = i[customRoleTypesInt] % 5;
|
|
buttonParent.localPosition = new Vector3(-3.47f + 1.75f * col, 1.5f - 0.45f * row, -200f);
|
|
buttonParent.localScale = new Vector3(0.55f, 0.55f, 1f);
|
|
label.text = GetString(role.ToString());
|
|
label.color = Utils.GetRoleColor(role);
|
|
label.alignment = TextAlignmentOptions.Center;
|
|
label.transform.localPosition = new Vector3(0, 0, label.transform.localPosition.z);
|
|
label.transform.localScale *= 1.6f;
|
|
label.autoSizeTextContainer = true;
|
|
|
|
var component = button.GetComponent<PassiveButton>();
|
|
component.OnClick.RemoveAllListeners();
|
|
|
|
if (PlayerControl.LocalPlayer.IsAlive())
|
|
{
|
|
component.OnClick.AddListener(() =>
|
|
{
|
|
if (selectedButton != button)
|
|
{
|
|
selectedButton = button;
|
|
buttons.ForEach(x => x.GetComponent<SpriteRenderer>().color = x == selectedButton ? Utils.GetRoleColor(PlayerControl.LocalPlayer.GetCustomRole()) : Main.DarkThemeForMeetingUI.Value ? new Color(0.1f, 0.1f, 0.1f) : Color.white);
|
|
}
|
|
else
|
|
{
|
|
if (MeetingHud.Instance.state is MeetingHud.MeetingStates.Results or MeetingHud.MeetingStates.Proceeding || !PlayerControl.LocalPlayer.IsAlive()) return;
|
|
|
|
Logger.Msg($"Click: {pc.GetNameWithRole()} => {role}", "Guesser UI");
|
|
|
|
if (specificRoleChoosing)
|
|
{
|
|
Loner.ProcessGuesserUI(playerId, role);
|
|
Changeling.ProcessGuesserUI(role);
|
|
Inquirer.ProcessGuesserUI(playerId, role);
|
|
Forger.ProcessGuesserUI(playerId, role);
|
|
Pawn.ProcessGuesserUI(role);
|
|
}
|
|
else
|
|
{
|
|
if (AmongUsClient.Instance.AmHost) GuesserMsg(PlayerControl.LocalPlayer, $"/bt {playerId} {GetString(role.ToString())}", true);
|
|
else SendRPC(playerId, role);
|
|
}
|
|
|
|
// Reset the GUI
|
|
__instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(true));
|
|
Object.Destroy(container.gameObject);
|
|
TextTemplate.enabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
i[customRoleTypesInt]++;
|
|
}
|
|
|
|
container.transform.localScale *= 0.75f;
|
|
GuesserSelectRole(specificRoleChoosing ? CustomRoleTypes.Impostor : CustomRoleTypes.Neutral);
|
|
ReloadPage();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Exception(ex, "Guesser UI");
|
|
return;
|
|
}
|
|
|
|
CustomSoundsManager.Play("Gunload");
|
|
}
|
|
|
|
private static bool ShowRoleOnUI(CustomRoles role, bool specificRoleChoosing = false)
|
|
{
|
|
if (role is CustomRoles.GM or CustomRoles.NotAssigned )
|
|
return false;
|
|
|
|
if (role is
|
|
CustomRoles.Ankylosaurus or
|
|
CustomRoles.BananaMan or
|
|
CustomRoles.Car or
|
|
CustomRoles.Disco or
|
|
CustomRoles.Flash or
|
|
CustomRoles.Giant or
|
|
CustomRoles.LastImpostor or
|
|
CustomRoles.Shifter or
|
|
CustomRoles.Specter or
|
|
CustomRoles.SuperStar
|
|
)
|
|
return specificRoleChoosing;
|
|
|
|
if (role.IsForOtherGameMode()) return false;
|
|
if (!role.IsEnable() && !role.RoleExist(true) && !CanMakeRoleSpawn(role)) return false;
|
|
return Options.CurrentGameMode == CustomGameMode.Standard && !CustomHnS.AllHnSRoles.Contains(role) && !role.IsGhostRole() && !role.IsVanilla();
|
|
|
|
bool CanMakeRoleSpawn(CustomRoles r)
|
|
{
|
|
Dictionary<CustomRoles, CustomRoles> d = new()
|
|
{
|
|
[CustomRoles.Pestilence] = CustomRoles.PlagueBearer,
|
|
[CustomRoles.VengefulRomantic] = CustomRoles.Romantic,
|
|
[CustomRoles.RuthlessRomantic] = CustomRoles.Romantic,
|
|
[CustomRoles.Deathknight] = CustomRoles.Necromancer,
|
|
[CustomRoles.Undead] = CustomRoles.Necromancer,
|
|
[CustomRoles.Sidekick] = CustomRoles.Jackal,
|
|
[CustomRoles.Charmed] = CustomRoles.Cultist,
|
|
[CustomRoles.Contagious] = CustomRoles.Virus,
|
|
[CustomRoles.Entranced] = CustomRoles.Siren
|
|
};
|
|
|
|
return d.TryGetValue(r, out var baseRole) && baseRole.RoleExist(true);
|
|
}
|
|
}
|
|
|
|
// Modded non-host client guess Role/Add-on
|
|
private static void SendRPC(int playerId, CustomRoles role)
|
|
{
|
|
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (int)CustomRPC.Guess, SendOption.Reliable, AmongUsClient.Instance.HostId);
|
|
writer.Write(playerId);
|
|
writer.Write((int)role);
|
|
AmongUsClient.Instance.FinishRpcImmediately(writer);
|
|
}
|
|
|
|
public static void ReceiveRPC(MessageReader reader, PlayerControl pc)
|
|
{
|
|
Logger.Msg($"{pc.GetNameWithRole()}", "GuessManager - PlayerControl pc");
|
|
|
|
int playerId = reader.ReadInt32();
|
|
Logger.Msg($"{playerId}", "GuessManager - Player Id");
|
|
|
|
var role = (CustomRoles)reader.ReadInt32();
|
|
Logger.Msg($"{role}", "GuessManager - Role Int32");
|
|
|
|
string roleStr = GetString(role.ToString());
|
|
Logger.Msg($"{roleStr}", "GuessManager - Role String");
|
|
|
|
GuesserMsg(pc, $"/bt {playerId} {roleStr}", true);
|
|
}
|
|
|
|
//[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
|
|
public static class StartMeetingPatch
|
|
{
|
|
//[SuppressMessage("ReSharper", "UnusedMember.Local")]
|
|
public static void Postfix(MeetingHud __instance)
|
|
{
|
|
bool restrictions = Options.GuesserNumRestrictions.GetBool();
|
|
|
|
if (AmongUsClient.Instance.AmHost)
|
|
{
|
|
Main.GuesserGuessedMeeting.SetAllValues(0);
|
|
|
|
if (Guessers.Count == 0 && restrictions)
|
|
InitializeGuesserPlayers();
|
|
|
|
HashSet<byte> guessers = Main.EnumerateAlivePlayerControls().Where(x => !x.IsModdedClient() && CanGuess(x, restrictions)).Select(x => x.PlayerId).ToHashSet();
|
|
bool meetingSS = Options.UseMeetingShapeshift.GetBool() && Options.UseMeetingShapeshiftForGuessing.GetBool();
|
|
LateTask.New(() => guessers.Do(x => Utils.SendMessage(GetString(meetingSS ? "YouCanGuessMeetingSS" : "YouCanGuess"), x, GetString("YouCanGuessTitle"), importance: restrictions && MeetingStates.FirstMeeting ? MessageImportance.High : MessageImportance.Medium)), 12f, log: false);
|
|
if (meetingSS) Data = guessers.ToDictionary(x => x, x => new MeetingShapeshiftData(x));
|
|
}
|
|
|
|
PlayerControl lp = PlayerControl.LocalPlayer;
|
|
if (!lp.IsAlive()) return;
|
|
|
|
if (CanGuess(lp, restrictions))
|
|
CreateGuesserButton(__instance);
|
|
}
|
|
|
|
public static bool CanGuess(PlayerControl lp, bool restrictions)
|
|
{
|
|
if ((!Options.UseMeetingShapeshift.GetBool() || !Options.UseMeetingShapeshiftForGuessing.GetBool()) && Banshee.Instances != null && Banshee.Instances.Exists(x => x.ScreechedPlayers.Contains(lp.PlayerId))) return false; // Vanilla clients can't guess with their chat hidden, so don't let modded clients guess for fairness
|
|
return lp.Is(CustomRoles.Guesser) || lp.GetCustomRole() switch
|
|
{
|
|
CustomRoles.EvilGuesser => true,
|
|
CustomRoles.NiceGuesser => true,
|
|
CustomRoles.NecroGuesser => true,
|
|
CustomRoles.Augur => true,
|
|
CustomRoles.Doomsayer when !Doomsayer.CantGuess => true,
|
|
CustomRoles.Decryptor when Decryptor.GuessMode.GetValue() == 2 => true,
|
|
_ when Options.GuesserMode.GetBool() => lp.GetTeam() switch
|
|
{
|
|
Team.Impostor => Options.ImpostorsCanGuess.GetBool(),
|
|
Team.Crewmate => Options.CrewmatesCanGuess.GetBool(),
|
|
Team.Neutral when lp.IsNeutralKiller() => Options.NeutralKillersCanGuess.GetBool(),
|
|
Team.Neutral => Options.PassiveNeutralsCanGuess.GetBool(),
|
|
Team.Coven => Options.CovenCanGuess.GetBool(),
|
|
_ => false
|
|
} && !(restrictions && !Guessers.Contains(lp.PlayerId)),
|
|
_ => false
|
|
};
|
|
}
|
|
|
|
private static void InitializeGuesserPlayers()
|
|
{
|
|
Dictionary<Team, List<PlayerControl>> players = Main.EnumeratePlayerControls()
|
|
.GroupBy(x => x.GetTeam())
|
|
.ToDictionary(x => x.Key, x => x.Shuffle());
|
|
|
|
foreach ((Team team, (OptionItem minSetting, OptionItem maxSetting)) in Options.NumGuessersOnEachTeam)
|
|
{
|
|
if (!players.TryGetValue(team, out List<PlayerControl> teamPlayers)) continue;
|
|
int num = IRandom.Instance.Next(minSetting.GetInt(), maxSetting.GetInt() + 1);
|
|
if (team == Team.Neutral) teamPlayers.Sort((x, y) => y.IsNeutralKiller().CompareTo(x.IsNeutralKiller()));
|
|
Guessers.UnionWith(teamPlayers.Take(num).Select(x => x.PlayerId));
|
|
}
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.OnDestroy))]
|
|
private static class MeetingHudOnDestroyGuesserUIClose
|
|
{
|
|
[SuppressMessage("ReSharper", "UnusedMember.Local")]
|
|
public static void Postfix()
|
|
{
|
|
if (TextTemplate && TextTemplate.gameObject) Object.Destroy(TextTemplate.gameObject);
|
|
TextTemplate = null;
|
|
|
|
if (GuesserUI) Object.Destroy(GuesserUI);
|
|
GuesserUI = null;
|
|
|
|
if (RoleButtons != null)
|
|
{
|
|
foreach (List<Transform> roleButtonsValue in RoleButtons.Values)
|
|
{
|
|
foreach (Transform transform in roleButtonsValue)
|
|
{
|
|
if (transform && transform.gameObject)
|
|
Object.Destroy(transform.gameObject);
|
|
}
|
|
}
|
|
|
|
RoleButtons = null;
|
|
}
|
|
|
|
if (RoleSelectButtons != null)
|
|
{
|
|
foreach (SpriteRenderer spriteRenderer in RoleSelectButtons.Values)
|
|
{
|
|
if (spriteRenderer && spriteRenderer.gameObject)
|
|
Object.Destroy(spriteRenderer.gameObject);
|
|
}
|
|
|
|
RoleSelectButtons = null;
|
|
}
|
|
|
|
if (PageButtons != null)
|
|
{
|
|
foreach (SpriteRenderer spriteRenderer in PageButtons)
|
|
{
|
|
if (spriteRenderer && spriteRenderer.gameObject)
|
|
Object.Destroy(spriteRenderer.gameObject);
|
|
}
|
|
|
|
PageButtons = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------
|
|
// Meeting Shapeshift for Guessing
|
|
// ----------------------------------------------------------------------------------------
|
|
|
|
public class MeetingShapeshiftData(byte guesserId)
|
|
{
|
|
private enum State
|
|
{
|
|
WaitingForTargetSelection,
|
|
TeamSelection,
|
|
FirstLetterSelection,
|
|
RoleSelection
|
|
}
|
|
|
|
private State CurrentState = State.WaitingForTargetSelection;
|
|
private PlayerControl Target;
|
|
private CustomRoleTypes CurrentTeam;
|
|
private CustomRoles[] ShownRoles;
|
|
private List<CustomRoles> CurrentRoles;
|
|
private CustomRoles SelectedRole;
|
|
private readonly List<ShapeshiftMenuElement> ExistingCNOs = [];
|
|
private readonly Dictionary<uint, string> NetIdToRawDisplay = [];
|
|
|
|
public void Reset()
|
|
{
|
|
try
|
|
{
|
|
if (CurrentState == State.WaitingForTargetSelection) return;
|
|
CurrentState = State.WaitingForTargetSelection;
|
|
Target = null;
|
|
CurrentTeam = default(CustomRoleTypes);
|
|
ExistingCNOs.Do(x => x.Despawn());
|
|
ExistingCNOs.Clear();
|
|
NetIdToRawDisplay.Clear();
|
|
PlayerControl pc = guesserId.GetPlayer();
|
|
if (pc) Utils.SendGameDataTo(pc.OwnerId);
|
|
Logger.Msg($"Reset Meeting Shapeshift Menu For Guessing ({Main.AllPlayerNames.GetValueOrDefault(guesserId, "Someone")})", "Meeting Shapeshift For Guessing");
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
}
|
|
|
|
public void AdvanceStep(PlayerControl target)
|
|
{
|
|
try
|
|
{
|
|
Logger.Info($"Advancing Step ({Main.AllPlayerNames.GetValueOrDefault(guesserId, "Someone")}, from {CurrentState})", "Meeting Shapeshift For Guessing");
|
|
|
|
switch (CurrentState)
|
|
{
|
|
case State.WaitingForTargetSelection:
|
|
{
|
|
Target = target;
|
|
CurrentState = State.TeamSelection;
|
|
SpawnCNOs();
|
|
break;
|
|
}
|
|
case State.TeamSelection:
|
|
{
|
|
if (!TryGetDisplay(out string display)) return;
|
|
|
|
if (display == "Cancel")
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
|
|
CurrentTeam = Enum.Parse<CustomRoleTypes>(display, true);
|
|
ShownRoles = Main.CustomRoleValues.Where(x => x.GetCustomRoleTypes() == CurrentTeam && ShowRoleOnUI(x)).ToArray();
|
|
CurrentState = State.FirstLetterSelection;
|
|
SpawnCNOs();
|
|
break;
|
|
}
|
|
case State.FirstLetterSelection:
|
|
{
|
|
if (!TryGetDisplay(out string display)) return;
|
|
|
|
if (display == "Cancel")
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
|
|
CurrentRoles = ShownRoles.Select(x => (role: x, str: GetString(x.ToString()))).Where(x => display.Split('-').Any(y => x.str.StartsWith(y.Trim(), StringComparison.InvariantCultureIgnoreCase))).Select(x => x.role).ToList();
|
|
|
|
if (CurrentRoles.Count == 0)
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
|
|
if (CurrentRoles.Count == 1)
|
|
{
|
|
// Directly select if there's only one role
|
|
SelectedRole = CurrentRoles[0];
|
|
goto case State.RoleSelection;
|
|
}
|
|
|
|
CurrentState = State.RoleSelection;
|
|
SpawnCNOs();
|
|
break;
|
|
}
|
|
case State.RoleSelection:
|
|
{
|
|
if (SelectedRole == default(CustomRoles))
|
|
{
|
|
if (!TryGetDisplay(out string display)) return;
|
|
|
|
if (display == "Cancel")
|
|
{
|
|
Reset();
|
|
return;
|
|
}
|
|
|
|
SelectedRole = Enum.Parse<CustomRoles>(display, true);
|
|
}
|
|
|
|
GuesserMsg(guesserId.GetPlayer(), $"/bt {Target.PlayerId} {GetString(SelectedRole.ToString())}");
|
|
Reset();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
|
|
return;
|
|
|
|
bool TryGetDisplay(out string display)
|
|
{
|
|
if (!NetIdToRawDisplay.TryGetValue(target.NetId, out display))
|
|
display = string.Empty;
|
|
|
|
if (string.IsNullOrWhiteSpace(display))
|
|
{
|
|
Reset();
|
|
return false;
|
|
}
|
|
|
|
Logger.Info($"Raw display choice: {display}", $"Meeting Shapeshift For Guessing ({Main.AllPlayerNames.GetValueOrDefault(guesserId, "Someone")})");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public void SpawnCNOs()
|
|
{
|
|
try
|
|
{
|
|
IEnumerable<string> choices = CurrentState switch
|
|
{
|
|
State.TeamSelection => EnumHelper.GetNames<CustomRoleTypes>(),
|
|
State.FirstLetterSelection => BuildLetterGroups(ShownRoles.Select(x => GetString(x.ToString())).OrderBy(x => x)),
|
|
State.RoleSelection => CurrentRoles.Select(x => x.ToString()),
|
|
_ => []
|
|
};
|
|
|
|
IEnumerable<string> namePlateIds = CurrentState switch
|
|
{
|
|
State.TeamSelection => ["nameplate_ripple", "nameplate_seeker", "nameplate_Polus_Lava", "nameplate_Celeste", "nameplate0001"],
|
|
_ => Enumerable.Repeat(CurrentTeam switch
|
|
{
|
|
CustomRoleTypes.Impostor => "nameplate_seeker",
|
|
CustomRoleTypes.Crewmate => "nameplate_ripple",
|
|
CustomRoleTypes.Neutral => "nameplate_Polus_Lava",
|
|
CustomRoleTypes.Coven => "nameplate_Celeste",
|
|
CustomRoleTypes.Addon => "nameplate0001",
|
|
_ => ""
|
|
}, 14)
|
|
};
|
|
|
|
choices = choices.Prepend("Cancel");
|
|
namePlateIds = namePlateIds.Prepend("nameplate_candyCanePlate");
|
|
|
|
(string choice, string namePlateId)[] data = choices.Zip(namePlateIds, (choice, namePlateId) => (choice, namePlateId)).ToArray();
|
|
var alivePlayerControls = Main.CachedAlivePlayerControls();
|
|
int alivePlayerControlsLength = alivePlayerControls.Count - 1;
|
|
|
|
Logger.Info($"Set Up Meeting Shapeshift Menu For Guessing ({Main.AllPlayerNames.GetValueOrDefault(guesserId, "Someone")}, {CurrentState})", "Meeting Shapeshift For Guessing");
|
|
|
|
// First, use living players to show choices by changing their names
|
|
// The local player can't be used to show a choice (-1)
|
|
|
|
StringBuilder sb = new();
|
|
int textIndex = 0;
|
|
|
|
int messages = 0;
|
|
var skipped = false;
|
|
PlayerControl guesser = guesserId.GetPlayer();
|
|
MessageWriter writer = MessageWriter.Get(SendOption.Reliable);
|
|
writer.StartMessage(6);
|
|
writer.Write(AmongUsClient.Instance.GameId);
|
|
writer.WritePacked(guesser.OwnerId);
|
|
|
|
for (var i = 0; i < alivePlayerControls.Count && (skipped ? i - 1 : i) < data.Length; i++)
|
|
{
|
|
string choice = data[skipped ? i - 1 : i].choice;
|
|
//string namePlateId = data[skipped ? i - 1 : i].namePlateId;
|
|
PlayerControl pc = alivePlayerControls[i];
|
|
|
|
if (pc.PlayerId == guesserId)
|
|
{
|
|
skipped = true;
|
|
continue;
|
|
}
|
|
|
|
NetIdToRawDisplay[pc.NetId] = choice;
|
|
string playerName = CurrentState == State.FirstLetterSelection ? choice : GetString(choice).ToUpper();
|
|
|
|
sb.Append($"[{playerName}]");
|
|
textIndex++;
|
|
|
|
if (textIndex % 3 == 0) sb.AppendLine();
|
|
else sb.Append(' ');
|
|
|
|
if (writer.Length > 500 || messages >= AmongUsClient.Instance.GetMaxMessagePackingLimit())
|
|
{
|
|
messages = 0;
|
|
writer.EndMessage();
|
|
AmongUsClient.Instance.SendOrDisconnect(writer);
|
|
writer.Clear(SendOption.Reliable);
|
|
writer.StartMessage(6);
|
|
writer.Write(AmongUsClient.Instance.GameId);
|
|
writer.WritePacked(guesser.OwnerId);
|
|
}
|
|
|
|
writer.StartMessage(2);
|
|
writer.WritePacked(pc.NetId);
|
|
writer.Write((byte)RpcCalls.SetName);
|
|
writer.Write(pc.Data.NetId);
|
|
writer.Write(playerName);
|
|
writer.Write(false);
|
|
writer.EndMessage();
|
|
|
|
messages++;
|
|
}
|
|
|
|
writer.EndMessage();
|
|
AmongUsClient.Instance.SendOrDisconnect(writer);
|
|
writer.Recycle();
|
|
|
|
// If there aren't enough living players, spawn new CNOs to show the rest of choices
|
|
|
|
// Since CNOs use the local player's NetworkedPlayerInfo, and AU reads the player's name directly from it,
|
|
// it's impossible to show vanilla players all choices accurately with CNOs.
|
|
// No workaround found yet....
|
|
// So we send the remaining choices in chat so the player can identify them
|
|
|
|
if (data.Length >= alivePlayerControlsLength)
|
|
{
|
|
for (int i = alivePlayerControlsLength; i < data.Length; i++)
|
|
{
|
|
string choice = data[i].choice;
|
|
//string namePlateId = data[i].namePlateId;
|
|
|
|
sb.Append($"[{(CurrentState == State.FirstLetterSelection ? choice : GetString(choice).ToUpper())}]");
|
|
textIndex++;
|
|
|
|
if (textIndex % 3 == 0) sb.AppendLine();
|
|
else sb.Append(' ');
|
|
|
|
// If there's an existing CNO, reuse it
|
|
ShapeshiftMenuElement cno;
|
|
|
|
if (ExistingCNOs.Count + alivePlayerControlsLength > i)
|
|
cno = ExistingCNOs[i - alivePlayerControlsLength];
|
|
else
|
|
{
|
|
cno = new ShapeshiftMenuElement(guesser);
|
|
ExistingCNOs.Add(cno);
|
|
}
|
|
|
|
NetIdToRawDisplay[cno.playerControl.NetId] = choice;
|
|
}
|
|
|
|
// Despawn unused CNOs
|
|
for (int i = data.Length - alivePlayerControlsLength; i < ExistingCNOs.Count; i++)
|
|
ExistingCNOs[i].Despawn();
|
|
|
|
ExistingCNOs.RemoveRange(data.Length - alivePlayerControlsLength, ExistingCNOs.Count - (data.Length - alivePlayerControlsLength));
|
|
|
|
Logger.Info($"Sent {data.Length - alivePlayerControlsLength} CNOs, Reused {ExistingCNOs.Count} Existing CNOs", "Meeting Shapeshift For Guessing");
|
|
}
|
|
else
|
|
{
|
|
ExistingCNOs.ForEach(x => x.Despawn());
|
|
ExistingCNOs.Clear();
|
|
}
|
|
|
|
Utils.SendMessage(sb.ToString().Trim(), guesserId, GetString($"ShapeshiftGuesserUITitle.{CurrentState}"), importance: MessageImportance.High);
|
|
|
|
Logger.Info($"Spawned {ExistingCNOs.Count} CNOs, Used {alivePlayerControlsLength} Living Players, Showing {data.Length} Choices", "Meeting Shapeshift For Guessing");
|
|
}
|
|
catch (Exception e) { Utils.ThrowException(e); }
|
|
}
|
|
|
|
// This problem goes beyond my ability to solve it perfectly, so I used AI
|
|
// Even this solution is not perfect, but it should be good enough for most cases
|
|
/// <summary>
|
|
/// Build up to maxGroups labels (like "[A-B]") from ordered roleNames so:
|
|
/// - each label covers contiguous starting-prefixes,
|
|
/// - no label contains more than maxItemsPerGroup roles,
|
|
/// - if a single starting-prefix has > maxItemsPerGroup roles, it will be subdivided by longer prefixes,
|
|
/// - tries to balance groups by merging adjacent buckets while respecting the maxItemsPerGroup limit.
|
|
/// </summary>
|
|
public static IEnumerable<string> BuildLetterGroups(
|
|
IEnumerable<string> roleNamesOrdered,
|
|
int maxGroups = 14,
|
|
int maxItemsPerGroup = 14,
|
|
CultureInfo culture = null)
|
|
{
|
|
culture ??= CultureInfo.CurrentCulture;
|
|
List<string> roles = roleNamesOrdered.ToList();
|
|
if (roles.Count == 0) yield break;
|
|
|
|
// Step 1: initial buckets grouped by first grapheme
|
|
var orderedKeys = new List<string>(); // preserve appearance order
|
|
var map = new Dictionary<string, List<string>>();
|
|
|
|
foreach (string r in roles)
|
|
{
|
|
string key = GetPrefix(r, 1);
|
|
|
|
if (!map.ContainsKey(key))
|
|
{
|
|
orderedKeys.Add(key);
|
|
map[key] = [];
|
|
}
|
|
|
|
map[key].Add(r);
|
|
}
|
|
|
|
// Represent buckets as list of (prefix, rolesList)
|
|
List<(string prefix, List<string> roles)> buckets = orderedKeys.Select(k => (prefix: k, roles: map[k])).ToList();
|
|
|
|
// Step 2: For any bucket with count > maxItemsPerGroup, subdivide it by increasing prefix length
|
|
for (var i = 0; i < buckets.Count; ++i)
|
|
{
|
|
if (buckets[i].roles.Count <= maxItemsPerGroup) continue;
|
|
|
|
List<string> tooBigRoles = buckets[i].roles;
|
|
var p = 2; // try second grapheme, third, ...
|
|
|
|
while (true)
|
|
{
|
|
var subOrder = new List<string>();
|
|
var subMap = new Dictionary<string, List<string>>();
|
|
|
|
foreach (string r in tooBigRoles)
|
|
{
|
|
string subKey = GetPrefix(r, p);
|
|
|
|
if (!subMap.ContainsKey(subKey))
|
|
{
|
|
subOrder.Add(subKey);
|
|
subMap[subKey] = [];
|
|
}
|
|
|
|
subMap[subKey].Add(r);
|
|
}
|
|
|
|
// If any sub-bucket still larger than maxItemsPerGroup, increase p and try again.
|
|
bool anyTooLarge = subMap.Values.Any(list => list.Count > maxItemsPerGroup);
|
|
|
|
if (!anyTooLarge)
|
|
{
|
|
// replace the single too-large bucket with its sub-buckets (in order)
|
|
var newList = new List<(string prefix, List<string> roles)>();
|
|
foreach (string k in subOrder) newList.Add((k, subMap[k]));
|
|
// replace in buckets
|
|
buckets.RemoveAt(i);
|
|
buckets.InsertRange(i, newList);
|
|
i += newList.Count - 1;
|
|
break;
|
|
}
|
|
|
|
p++;
|
|
// safeguard: if p grows beyond the longest role length, break to avoid infinite loop
|
|
int maxTextElements = Enumerable.Max(tooBigRoles, rr => StringInfo.ParseCombiningCharacters(rr).Length);
|
|
|
|
if (p > maxTextElements)
|
|
{
|
|
// As a last resort, split the list into chunks of maxItemsPerGroup preserving order
|
|
var finalSplit = new List<(string prefix, List<string> roles)>();
|
|
var idx = 0;
|
|
|
|
while (idx < tooBigRoles.Count)
|
|
{
|
|
List<string> slice = tooBigRoles.Skip(idx).Take(maxItemsPerGroup).ToList();
|
|
// prefix label use first and last role's prefix for clarity (not perfect but safe)
|
|
string label = GetPrefix(slice.First(), 1);
|
|
finalSplit.Add((label, slice));
|
|
idx += maxItemsPerGroup;
|
|
}
|
|
|
|
buckets.RemoveAt(i);
|
|
buckets.InsertRange(i, finalSplit);
|
|
i += finalSplit.Count - 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 3: If we have more buckets than maxGroups, merge adjacent buckets where possible
|
|
// We'll greedily merge the adjacent pair with smallest combined size that doesn't exceed maxItemsPerGroup,
|
|
// repeating until buckets.Count <= maxGroups or no mergeable pair exists.
|
|
while (buckets.Count > maxGroups)
|
|
{
|
|
int bestIdx = -1;
|
|
var bestCombinedSize = int.MaxValue;
|
|
|
|
for (var i = 0; i < buckets.Count - 1; ++i)
|
|
{
|
|
int combined = buckets[i].roles.Count + buckets[i + 1].roles.Count;
|
|
|
|
if (combined <= maxItemsPerGroup && combined < bestCombinedSize)
|
|
{
|
|
bestCombinedSize = combined;
|
|
bestIdx = i;
|
|
}
|
|
}
|
|
|
|
if (bestIdx == -1)
|
|
{
|
|
// No adjacent pair can be merged without exceeding maxItemsPerGroup.
|
|
// Absolutely diabolical.
|
|
// Here we choose to break and output as-is (caller gets <= buckets.Count labels, possibly > maxGroups).
|
|
break;
|
|
}
|
|
|
|
// merge buckets[bestIdx] and buckets[bestIdx+1]
|
|
var mergedRoles = new List<string>(buckets[bestIdx].roles.Count + buckets[bestIdx + 1].roles.Count);
|
|
mergedRoles.AddRange(buckets[bestIdx].roles);
|
|
mergedRoles.AddRange(buckets[bestIdx + 1].roles);
|
|
string mergedPrefix = buckets[bestIdx].prefix; // prefix string for merged block will be the first prefix (label will show range)
|
|
buckets[bestIdx] = (mergedPrefix, mergedRoles);
|
|
buckets.RemoveAt(bestIdx + 1);
|
|
}
|
|
|
|
// Step 4: build label strings for each bucket: if bucket covers multiple distinct prefixes, show "first-last" else show "first"
|
|
// But we might have buckets whose prefix string is identical for every role (common case).
|
|
foreach ((string prefix, List<string> roles) bucket in buckets)
|
|
{
|
|
// show all distinct prefixes in this bucket (e.g. "A-B-C" if roles are "Ant", "Bat", "Cat")
|
|
List<string> distinctPrefixes = bucket.roles.Select(r => GetPrefix(r, 1)).Distinct().ToList();
|
|
string label = distinctPrefixes.Count == 1 ? distinctPrefixes[0] : string.Join('-', distinctPrefixes);
|
|
yield return label;
|
|
}
|
|
|
|
yield break;
|
|
|
|
// Helper: get first p text elements (grapheme clusters), upper-cased by culture.
|
|
string GetPrefix(string s, int p)
|
|
{
|
|
if (string.IsNullOrEmpty(s)) return s;
|
|
var si = new StringInfo(s);
|
|
int take = Math.Min(p, si.LengthInTextElements);
|
|
return culture.TextInfo.ToUpper(si.SubstringByTextElements(0, take));
|
|
}
|
|
}
|
|
}
|
|
|
|
public static Dictionary<byte, MeetingShapeshiftData> Data = [];
|
|
|
|
public static void OnMeetingShapeshiftReceived(PlayerControl shapeshifter, PlayerControl target)
|
|
{
|
|
if (Data.TryGetValue(shapeshifter.PlayerId, out MeetingShapeshiftData msd))
|
|
msd.AdvanceStep(target);
|
|
}
|
|
} |