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