diff --git a/ChaosTokens/ChaosTokens.csproj b/ChaosTokens/ChaosTokens.csproj index 5894117..e8511b1 100644 --- a/ChaosTokens/ChaosTokens.csproj +++ b/ChaosTokens/ChaosTokens.csproj @@ -3,16 +3,15 @@ net6.0 latest embedded - Chipseq - 1.1.3 + 1.2.0 - + diff --git a/ChaosTokens/ChaosTokensRpc.cs b/ChaosTokens/ChaosTokensRpc.cs index 972bf35..654eb1a 100644 --- a/ChaosTokens/ChaosTokensRpc.cs +++ b/ChaosTokens/ChaosTokensRpc.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Linq; -using AmongUs.GameOptions; using ChaosTokens.Modifiers; using ChaosTokens.Modifiers.Effects; using ChaosTokens.Options; @@ -34,7 +33,7 @@ public enum RpcCalls : uint Roll, IncreaseTokens, DecreaseTokens, - + TokenPosSwap, TokenRoleSwap, TokenRevive, @@ -42,8 +41,9 @@ public enum RpcCalls : uint public static class ChaosTokensRpc { - private static int _revealsLeft = (int)OptionGroupSingleton.Instance.MaxRoleReveals; - + public static int RevealsLeft { get; set; } + public static int SwapsLeft { get; set; } + [MethodRpc((uint)RpcCalls.Roll)] public static void RpcRoll(this PlayerControl player) { @@ -54,27 +54,26 @@ public static void RpcRoll(this PlayerControl player) { return; } - - bool reroll = true; + + bool reroll; do { - reroll = false; int random = Random.RandomRangeInt(0, Enum.GetValues().Length); //Logger.Warning($"Rolled effect for {player.Data.PlayerName} - {(ChaosEffects)random}"); reroll = ApplyEffect(player, (ChaosEffects)random); } while (reroll); - RpcDecreaseTokens(player, 1); + player.RpcDecreaseTokens(1); } - + [MethodRpc((uint)RpcCalls.IncreaseTokens)] public static void RpcIncreaseTokens(this PlayerControl player, int amount, bool showNotification = false) { if (!player.TryGetModifier(out var chaosTokenModifier)) { //Logger.Error($"Cannot increase tokens: player {player.Data.PlayerName} has no modifier, adding one"); - chaosTokenModifier = player.AddModifier(amount, showNotification); + player.AddModifier(amount, showNotification); } else { @@ -90,16 +89,16 @@ public static void RpcDecreaseTokens(this PlayerControl player, int amount) Logger.Error($"Cannot decrease tokens: player {player.Data.PlayerName} has no modifier"); return; } - + chaosTokenModifier.DecreaseTokens(amount); } - + [MethodRpc((uint)RpcCalls.TokenPosSwap)] public static void RpcTokenPositionSwap(PlayerControl player, PlayerControl victim) { Vector3 pos1 = player.GetTruePosition(); Vector3 pos2 = victim.GetTruePosition(); - + TransporterRole.Transport(player, pos2); TransporterRole.Transport(victim, pos1); @@ -107,13 +106,13 @@ public static void RpcTokenPositionSwap(PlayerControl player, PlayerControl vict { Utils.Notification("You swapped positions with someone!"); } - + if (victim.AmOwner) { Utils.Notification("Someone swapped positions with you!"); } } - + [MethodRpc((uint)RpcCalls.TokenRoleSwap)] public static void RpcTokenRoleSwap(PlayerControl player, PlayerControl victim) { @@ -125,9 +124,9 @@ public static void RpcTokenRoleSwap(PlayerControl player, PlayerControl victim) { Utils.Notification("Someone swapped roles with you!"); } - - RoleTypes role1 = player.Data.Role.Role; - RoleTypes role2 = victim.Data.Role.Role; + + var role1 = player.Data.Role.Role; + var role2 = victim.Data.Role.Role; player.ChangeRole((ushort)role2); victim.ChangeRole((ushort)role1); @@ -157,7 +156,7 @@ void Reroll() { reroll = true; } - + var playerRole = player.Data.Role; switch (effect) { @@ -238,7 +237,7 @@ void Reroll() Reroll(); break; } - + if (!Utils.GetUncompletedTasks(player).Any()) { Reroll(); @@ -292,14 +291,14 @@ void Reroll() Reroll(); break; } - if (_revealsLeft <= 0) + if (RevealsLeft <= 0) { Reroll(); break; } player.RpcAddModifier(player.Data.Role.Role, player.Data.PlayerId); - _revealsLeft--; + RevealsLeft--; break; case ChaosEffects.Death: if (player.HasModifier()) @@ -340,8 +339,8 @@ void Reroll() Reroll(); break; } - - if (_revealsLeft <= 0) + + if (RevealsLeft <= 0) { Reroll(); break; @@ -351,7 +350,7 @@ void Reroll() .Where(r => r.Team != player.GetTownOfUsRole()?.Team) .Select(r => (r as RoleBehaviour).Role); player.RpcAddModifier(validRoles.Random(), player.Data.PlayerId); - _revealsLeft--; + RevealsLeft--; break; case ChaosEffects.Hyperactive: if (player.HasModifier()) @@ -410,7 +409,7 @@ void Reroll() */ case ChaosEffects.RevealRandom: - if (_revealsLeft <= 0) + if (RevealsLeft <= 0) { Reroll(); break; @@ -429,7 +428,7 @@ void Reroll() var revealVictim = revealVictims.Random(); revealVictim.RpcAddModifier(revealVictim.Data.Role.Role, player.PlayerId); - _revealsLeft--; + RevealsLeft--; break; case ChaosEffects.PositionSwap: var swapVictim = Helpers.GetAlivePlayers() @@ -439,6 +438,12 @@ void Reroll() RpcTokenPositionSwap(player, swapVictim); break; case ChaosEffects.RoleSwap: + if (SwapsLeft <= 0) + { + Reroll(); + break; + } + // This will be annoying as hell but the players have to be on the same team so it's kind of balanced ModdedRoleTeams playerTeam; if (playerRole is ICustomRole customRole) @@ -473,6 +478,7 @@ void Reroll() } RpcTokenRoleSwap(player, roleSwapVictims.Random()); + SwapsLeft--; break; case ChaosEffects.Revive: if (OptionGroupSingleton.Instance.ReviveDisabled) @@ -480,7 +486,7 @@ void Reroll() Reroll(); break; } - + var canBeRevived = PlayerControl.AllPlayerControls .ToArray() .Where(x => x.Data.IsDead && !x.Data.Disconnected) @@ -513,7 +519,7 @@ void Reroll() player.RpcAddModifier(); break; - + default: Reroll(); break; @@ -521,7 +527,7 @@ void Reroll() return reroll; } - + public static IEnumerator CoRevivePlayer(PlayerControl dead) { var roleWhenAlive = dead.GetRoleWhenAlive(); diff --git a/ChaosTokens/Modifiers/Effects/TokenAssassin.cs b/ChaosTokens/Modifiers/Effects/TokenAssassin.cs index 69bccf8..23da90e 100644 --- a/ChaosTokens/Modifiers/Effects/TokenAssassin.cs +++ b/ChaosTokens/Modifiers/Effects/TokenAssassin.cs @@ -1,9 +1,9 @@ using System.Linq; using HarmonyLib; +using Il2CppInterop.Runtime.Attributes; using MiraAPI.GameOptions; using MiraAPI.Modifiers; using MiraAPI.Networking; -using MiraAPI.Roles; using MiraAPI.Utilities; using Reactor.Utilities; using TownOfUs; @@ -20,21 +20,22 @@ using TownOfUs.Roles.Crewmate; using TownOfUs.Roles.Neutral; using TownOfUs.Utilities; -using UnityEngine; namespace ChaosTokens.Modifiers.Effects; public class TokenAssassin : TokenEffect { public override string ModifierName => "Token Assassin"; + public override string Notification => "You can attempt to guess a player's role during the next meeting."; public override ChaosEffects Effect => ChaosEffects.Assassin; public override bool Negative => false; - + public override bool RemoveAfterMeeting => true; + private MeetingMenu meetingMenu; public string LastGuessedItem { get; set; } - public PlayerControl? LastAttemptedVictim { get; set; } - private bool shot; - + [HideFromIl2Cpp] + public PlayerControl LastAttemptedVictim { get; set; } + public override void OnActivate() { base.OnActivate(); @@ -86,7 +87,7 @@ public void ClickGuess(PlayerVoteArea voteArea, MeetingHud meetingHud) { return; } - + if (Minigame.Instance != null) { return; @@ -137,27 +138,19 @@ void ClickHandler(PlayerControl victim) shapeMenu.Close(); LastGuessedItem = string.Empty; LastAttemptedVictim = null; - + Utils.Notification("Your target was blessed."); + Player.RpcRemoveModifier(); return; } - if (victim == Player && Player.TryGetModifier(out var modifier) && !modifier.Used) + if (victim == Player) { - modifier!.Used = true; - Coroutines.Start(MiscUtils.CoFlash(TownOfUsColors.Impostor)); - - var notif1 = Helpers.CreateAndShowNotification( - $"{TownOfUsColors.ImpSoft.ToTextColor()}Your Double Shot has prevented you from dying this meeting!", - Color.white, spr: TouModifierIcons.DoubleShot.LoadAsset()); - - notif1.Text.SetOutlineThickness(0.35f); - notif1.transform.localPosition = new Vector3(0f, 1f, -20f); - + Utils.Notification("Your guess was incorrect."); shapeMenu.Close(); LastGuessedItem = string.Empty; LastAttemptedVictim = null; - + Player.RpcRemoveModifier(); return; } @@ -171,19 +164,11 @@ void ClickHandler(PlayerControl victim) MeetingMenu.Instances.Do(x => x.HideSingle(victim.PlayerId)); DeathHandlerModifier.RpcUpdateDeathHandler(victim, "Guessed", DeathEventHandlers.CurrentRound, DeathHandlerOverride.SetFalse, $"By {Player.Data.PlayerName}", lockInfo: DeathHandlerOverride.SetTrue); } - else - { - DeathHandlerModifier.RpcUpdateDeathHandler(victim, "Misguessed", DeathEventHandlers.CurrentRound, DeathHandlerOverride.SetFalse, lockInfo: DeathHandlerOverride.SetTrue); - } - - if (!OptionGroupSingleton.Instance.AssassinMultiKill || victim == Player) - { - meetingMenu?.HideButtons(); - } shapeMenu.Close(); - meetingMenu.HideButtons(); - shot = true; + meetingMenu!.HideButtons(); + Player.RpcRemoveModifier(); + Utils.Notification("You have guessed correctly."); } } @@ -209,58 +194,15 @@ private bool IsRoleValid(RoleBehaviour role) return false; } - var options = OptionGroupSingleton.Instance; var touRole = role as ITownOfUsRole; - var assassinRole = Player.Data.Role as ITownOfUsRole; var unguessableRole = role as IUnguessable; - if (touRole is IGhostRole) + if (touRole is IGhostRole || unguessableRole is { IsGuessable: false }) { return false; } - if (unguessableRole != null && !unguessableRole.IsGuessable) - { - return false; - } - - if (touRole?.RoleAlignment == RoleAlignment.CrewmateInvestigative) - { - return options.AssassinGuessInvest; - } - - if (role.IsCrewmate() && role is ICustomRole) - { - return true; - } - - if (role.IsCrewmate() && OptionGroupSingleton.Instance.AssassinCrewmateGuess) - { - return true; - } - - if (role.IsImpostor() && OptionGroupSingleton.Instance.AssassinGuessImpostors && - assassinRole?.Team != ModdedRoleTeams.Impostor) - { - return true; - } - - if (touRole?.RoleAlignment == RoleAlignment.NeutralBenign) - { - return options.AssassinGuessNeutralBenign; - } - - if (touRole?.RoleAlignment == RoleAlignment.NeutralEvil) - { - return options.AssassinGuessNeutralEvil; - } - - if (touRole?.RoleAlignment == RoleAlignment.NeutralKilling) - { - return options.AssassinGuessNeutralKilling; - } - - return false; + return true; } private static bool IsModifierValid(BaseModifier modifier) diff --git a/ChaosTokens/Modifiers/Effects/TokenOneTimeKill.cs b/ChaosTokens/Modifiers/Effects/TokenOneTimeKill.cs index 9806f2f..9ce5037 100644 --- a/ChaosTokens/Modifiers/Effects/TokenOneTimeKill.cs +++ b/ChaosTokens/Modifiers/Effects/TokenOneTimeKill.cs @@ -7,7 +7,7 @@ public class TokenOneTimeKill : TokenEffect { public override ChaosEffects Effect => ChaosEffects.KillButton; public override string ModifierName => "Token Kill Button"; - public override string Notification => "You've gained a one-time-use kill button, take things into your own hands!"; + public override string Notification => "You've gained a one-time-use kill button, use it wisely!"; public override bool Negative => false; public override void OnActivate() diff --git a/ChaosTokens/Options/BalanceOptions.cs b/ChaosTokens/Options/BalanceOptions.cs index 4ead2b5..547291d 100644 --- a/ChaosTokens/Options/BalanceOptions.cs +++ b/ChaosTokens/Options/BalanceOptions.cs @@ -14,15 +14,18 @@ public class BalanceOptions : AbstractOptionGroup [ModdedToggleOption("Disable Death Token")] public bool DeathDisabled { get; set; } = false; - + [ModdedToggleOption("Disable Revival")] public bool ReviveDisabled { get; set; } = false; - + /* [ModdedToggleOption("Disable Screen Effects")] public bool ScreenEffectsDisabled { get; set; } = false; */ - + [ModdedNumberOption("Max Role Reveals", 0, 15, zeroInfinity: true)] public float MaxRoleReveals { get; set; } = 0; + + [ModdedNumberOption("Max Role Swaps", 0, 15, zeroInfinity: true)] + public float MaxRoleSwaps { get; set; } = 0; } \ No newline at end of file diff --git a/ChaosTokens/Options/TokenHandingOptions.cs b/ChaosTokens/Options/TokenHandingOptions.cs index 79a57c5..4a696f6 100644 --- a/ChaosTokens/Options/TokenHandingOptions.cs +++ b/ChaosTokens/Options/TokenHandingOptions.cs @@ -16,19 +16,19 @@ public class TokenHandingOptions : AbstractOptionGroup [ModdedToggleOption("Hand Tokens First Round")] public bool TokensEnabledFirstRound { get; set; } = true; - - [ModdedToggleOption("Enable Token Handing Priority")] - public bool TokenHandingPriority { get; set; } = true; - + + [ModdedToggleOption("Enable Weighted Token")] + public bool WeightedTokens { get; set; } = true; + [ModdedNumberOption("Handed Tokens Min", 1, 30)] public float TokensMin { get; set; } = 5; - + [ModdedNumberOption("Handed Tokens Max", 0, 30, zeroInfinity: true)] public float TokensMax { get; set; } = 10; - + [ModdedNumberOption("Initial Double Token Chance", 0, 100, 10, MiraNumberSuffixes.Percent)] public float InitialDoubleTokenChance { get; set; } = 10; - + [ModdedNumberOption("Double Token Chance Increase Per Round", 5, 50, 5, MiraNumberSuffixes.Percent)] public float DoubleTokenIncrease { get; set; } = 10; } \ No newline at end of file diff --git a/ChaosTokens/Patches/SpeedPatch.cs b/ChaosTokens/Patches/SpeedPatch.cs index 2049f5e..245b439 100644 --- a/ChaosTokens/Patches/SpeedPatch.cs +++ b/ChaosTokens/Patches/SpeedPatch.cs @@ -8,11 +8,8 @@ namespace ChaosTokens.Patches; [HarmonyPatch(typeof(LogicOptions), nameof(LogicOptions.GetPlayerSpeedMod))] public static class PlayerSpeedPatch { - // ReSharper disable once InconsistentNaming public static void Postfix(PlayerControl pc, ref float __result) { - __result *= pc.GetAppearance().Speed; - if (pc.TryGetModifier(out var tokenSpeed)) { __result *= tokenSpeed.Speed; diff --git a/ChaosTokens/TokenEvents.cs b/ChaosTokens/TokenEvents.cs index b50c384..5b79658 100644 --- a/ChaosTokens/TokenEvents.cs +++ b/ChaosTokens/TokenEvents.cs @@ -25,16 +25,17 @@ namespace ChaosTokens; public static class TokenEvents { private static float DoubleTokenChance { get; set; } = OptionGroupSingleton.Instance.InitialDoubleTokenChance; - private static readonly Dictionary PriorityTable = new(); - + private static readonly Dictionary WeightTable = new(); + [RegisterEvent] public static void HandleVotesEventHandler(HandleVoteEvent @event) { if (@event.TargetId == 253 && ModifierUtils.GetPlayersWithModifier().Any()) { @event.Cancel(); + return; } - + if (@event.VoteData.Owner.HasModifier()) { @event.VoteData.SetRemainingVotes(0); @@ -63,7 +64,7 @@ public static void HandleVotesEventHandler(HandleVoteEvent @event) @event.Cancel(); } } - + [RegisterEvent] public static void VotingCompleteEventHandler(VotingCompleteEvent @event) { @@ -71,7 +72,7 @@ public static void VotingCompleteEventHandler(VotingCompleteEvent @event) { player.CustomMurder(player, MurderResultFlags.Succeeded, createDeadBody: false, showKillAnim: false); DeathHandlerModifier.UpdateDeathHandler(player, "Fate"); - + // ToUM adds time when someone dies during a meeting, because of that the proceed anim is longer // We add the time it subtracted to fix this var timer = (int)OptionGroupSingleton.Instance.AddedMeetingDeathTimer; @@ -123,75 +124,55 @@ public static void RoundStartEventHandler(RoundStartEvent @event) } if (@event.TriggeredByIntro) { - PriorityTable.Clear(); + WeightTable.Clear(); PlayerControl.AllPlayerControls .ToArray() - .Do(p => PriorityTable.Add(p.PlayerId, 10)); - + .Do(p => WeightTable.Add(p.PlayerId, 10)); + if (!OptionGroupSingleton.Instance.TokensEnabledFirstRound) { return; } } - int min = (int)OptionGroupSingleton.Instance.TokensMin; - int max = (int)OptionGroupSingleton.Instance.TokensMax; - + var min = (int)OptionGroupSingleton.Instance.TokensMin; + var max = (int)OptionGroupSingleton.Instance.TokensMax; if (max == 0) { max = int.MaxValue; } - if (min > max) { min = max; } var potentialPlayers = Helpers.GetAlivePlayers(); - potentialPlayers.Do(p => - { - // Just to be sure - if (!PriorityTable.ContainsKey(p.PlayerId)) - { - PriorityTable.Add(p.PlayerId, 10); - } - }); - potentialPlayers.Shuffle(); - potentialPlayers = potentialPlayers.OrderBy(x => PriorityTable[x.PlayerId]).ToList(); - - int tokensToHandle = Mathf.Clamp(Random.RandomRangeInt(min, max), 0, potentialPlayers.Count); - foreach (var player in potentialPlayers.Clone()) - { - // Reset when player gets a token - PriorityTable[player.PlayerId] = 10; - - int tokens = 1; - if (Random.RandomRangeInt(1, 100) < DoubleTokenChance && tokensToHandle > 1) - { - tokens = 2; - PriorityTable[player.PlayerId] = 11; // double token decreases the priority - } + min = Mathf.Min(min, potentialPlayers.Count); + max = Mathf.Min(max, potentialPlayers.Count); + var tokensToHand = Mathf.Min(Random.RandomRangeInt(min, max + 1), potentialPlayers.Count); + var winners = WeightedSample(potentialPlayers, WeightTable, tokensToHand, 3); + foreach (var player in winners) + { + int tokens = Random.RandomRangeInt(1, 100) < DoubleTokenChance ? 2 : 1; player.RpcIncreaseTokens(tokens, true); - - tokensToHandle -= tokens; - potentialPlayers.Remove(player); - if (tokensToHandle <= 0) - { - break; - } } - - // Players without a token get priority for the next handing - potentialPlayers.Do(p => PriorityTable[p.PlayerId]--); - // Simplest solution to ignore handing priority: reset everything if disabled - if (!OptionGroupSingleton.Instance.TokenHandingPriority) + // i mean if we reset the weights table, everyone's chance is still the same? + if (!OptionGroupSingleton.Instance.WeightedTokens) { - potentialPlayers.Do(p => PriorityTable[p.PlayerId] = 10); + potentialPlayers.Do(p => WeightTable[p.PlayerId] = 10); } - - DoubleTokenChance = Mathf.Clamp(DoubleTokenChance + OptionGroupSingleton.Instance.DoubleTokenIncrease, 0, 100); + + var doubleChance = DoubleTokenChance + OptionGroupSingleton.Instance.DoubleTokenIncrease; + DoubleTokenChance = Mathf.Clamp(doubleChance, 0, 100); + } + + [RegisterEvent] + public static void GameStartEventRegister(IntroBeginEvent @event) + { + ChaosTokensRpc.RevealsLeft = (int)OptionGroupSingleton.Instance.MaxRoleReveals; + ChaosTokensRpc.SwapsLeft = (int)OptionGroupSingleton.Instance.MaxRoleSwaps; } [RegisterEvent] @@ -210,7 +191,7 @@ public static void BeforeMurderEventHandler(BeforeMurderEvent @event) { return; } - + @event.Cancel(); if (source.AmOwner) { @@ -220,4 +201,43 @@ public static void BeforeMurderEventHandler(BeforeMurderEvent @event) } } } + + private static PlayerControl[] WeightedSample(List players, Dictionary weights, int n, float adjustment) + { + var pool = new List(players); + var winners = new List(); + var rng = new System.Random(); + + for (int i = 0; i < n; i++) + { + float total = pool.Sum(p => weights[p.PlayerId]); + float roll = (float)rng.NextDouble() * total; + float cumulative = 0; + + foreach (var player in pool) + { + cumulative += weights[player.PlayerId]; + if (roll < cumulative) + { + winners.Add(player); + pool.Remove(player); + break; + } + } + } + + foreach (var player in players) + { + if (winners.Contains(player)) + { + weights[player.PlayerId] = Mathf.Max(1, weights[player.PlayerId] - adjustment); + } + else + { + weights[player.PlayerId] += adjustment; + } + } + + return winners.ToArray(); + } } \ No newline at end of file diff --git a/ChaosTokens/libs/TownOfUsMira.dll b/ChaosTokens/libs/TownOfUsMira.dll index 3d8d676..11a5b4d 100644 Binary files a/ChaosTokens/libs/TownOfUsMira.dll and b/ChaosTokens/libs/TownOfUsMira.dll differ