diff --git a/ChaosTokens/Buttons/RollButton.cs b/ChaosTokens/Buttons/RollButton.cs index d506ef8..0a404cb 100644 --- a/ChaosTokens/Buttons/RollButton.cs +++ b/ChaosTokens/Buttons/RollButton.cs @@ -2,6 +2,7 @@ using ChaosTokens.Options; using MiraAPI.GameOptions; using MiraAPI.Hud; +using MiraAPI.Keybinds; using MiraAPI.Modifiers; using MiraAPI.Utilities.Assets; using TownOfUs.Buttons; @@ -17,7 +18,6 @@ public class RollButton : TownOfUsButton public override LoadableAsset Sprite => Assets.DiceButton; public override Color TextOutlineColor => ChaosTokensPlugin.MainColor; public override ButtonLocation Location => ButtonLocation.BottomLeft; - public override string Keybind => Keybinds.ModifierAction; protected override void OnClick() { diff --git a/ChaosTokens/ChaosEffects.cs b/ChaosTokens/ChaosEffects.cs index bab14a4..35a0ade 100644 --- a/ChaosTokens/ChaosEffects.cs +++ b/ChaosTokens/ChaosEffects.cs @@ -14,6 +14,7 @@ public enum ChaosEffects Tasks, Vision, Invisible, + Assassin, // Negative RevealSelf, @@ -22,8 +23,10 @@ public enum ChaosEffects Drunk, FakeRevealSelf, Hyperactive, - Nausea, + Nausea, // Unused Colorblind, + ScreenFlip, // Unused + Blind, // Unused // Neutral RevealRandom, diff --git a/ChaosTokens/ChaosTokens.csproj b/ChaosTokens/ChaosTokens.csproj index c8aac55..5894117 100644 --- a/ChaosTokens/ChaosTokens.csproj +++ b/ChaosTokens/ChaosTokens.csproj @@ -5,14 +5,14 @@ embedded Chipseq - 1.1.2 + 1.1.3 - + - + diff --git a/ChaosTokens/ChaosTokensRpc.cs b/ChaosTokens/ChaosTokensRpc.cs index 8fe22a0..972bf35 100644 --- a/ChaosTokens/ChaosTokensRpc.cs +++ b/ChaosTokens/ChaosTokensRpc.cs @@ -17,6 +17,7 @@ using TownOfUs; using TownOfUs.Events; using TownOfUs.Modifiers; +using TownOfUs.Modifiers.Game; using TownOfUs.Modifiers.Game.Alliance; using TownOfUs.Modifiers.Impostor; using TownOfUs.Modules; @@ -269,6 +270,20 @@ void Reroll() player.RpcAddModifier(); break; + case ChaosEffects.Assassin: + if (player.HasModifier()) + { + Reroll(); + break; + } + if (player.HasModifier()) + { + Reroll(); + break; + } + + player.RpcAddModifier(); + break; case ChaosEffects.RevealSelf: @@ -373,7 +388,26 @@ void Reroll() player.RpcAddModifier(); break; + /* + case ChaosEffects.ScreenFlip: + if (player.HasModifier()) + { + Reroll(); + break; + } + + player.RpcAddModifier(); + break; + case ChaosEffects.Blind: + if (player.HasModifier() || player.HasModifier()) + { + Reroll(); + break; + } + player.RpcAddModifier(); + break; + */ case ChaosEffects.RevealRandom: if (_revealsLeft <= 0) @@ -479,6 +513,10 @@ void Reroll() player.RpcAddModifier(); break; + + default: + Reroll(); + break; } return reroll; diff --git a/ChaosTokens/Modifiers/Effects/TokenAssassin.cs b/ChaosTokens/Modifiers/Effects/TokenAssassin.cs new file mode 100644 index 0000000..69bccf8 --- /dev/null +++ b/ChaosTokens/Modifiers/Effects/TokenAssassin.cs @@ -0,0 +1,308 @@ +using System.Linq; +using HarmonyLib; +using MiraAPI.GameOptions; +using MiraAPI.Modifiers; +using MiraAPI.Networking; +using MiraAPI.Roles; +using MiraAPI.Utilities; +using Reactor.Utilities; +using TownOfUs; +using TownOfUs.Assets; +using TownOfUs.Events; +using TownOfUs.Extensions; +using TownOfUs.Modifiers; +using TownOfUs.Modifiers.Crewmate; +using TownOfUs.Modifiers.Game; +using TownOfUs.Modules; +using TownOfUs.Modules.Components; +using TownOfUs.Options; +using TownOfUs.Roles; +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 ChaosEffects Effect => ChaosEffects.Assassin; + public override bool Negative => false; + + private MeetingMenu meetingMenu; + public string LastGuessedItem { get; set; } + public PlayerControl? LastAttemptedVictim { get; set; } + private bool shot; + + public override void OnActivate() + { + base.OnActivate(); + + //Logger.Error($"AssassinModifier.OnActivate maxKills: {maxKills}"); + if (Player.AmOwner) + { + meetingMenu = new MeetingMenu( + Player.Data.Role, + ClickGuess, + MeetingAbilityType.Click, + TouAssets.Guess, + null!, + IsExempt); + } + } + + public override void OnMeetingStart() + { + base.OnMeetingStart(); + if (Player.AmOwner) + { + meetingMenu.GenButtons(MeetingHud.Instance, + Player.AmOwner && !Player.HasDied() && !Player.HasModifier()); + } + } + + public void OnVotingComplete() + { + if (Player.AmOwner) + { + meetingMenu?.Dispose(); + } + } + + public override void OnDeactivate() + { + base.OnDeactivate(); + if (Player.AmOwner) + { + meetingMenu?.Dispose(); + meetingMenu = null!; + } + } + + public void ClickGuess(PlayerVoteArea voteArea, MeetingHud meetingHud) + { + if (meetingHud.state == MeetingHud.VoteStates.Discussion) + { + return; + } + + if (Minigame.Instance != null) + { + return; + } + + var player = GameData.Instance.GetPlayerById(voteArea.TargetPlayerId).Object; + + var shapeMenu = GuesserMenu.Create(); + shapeMenu.Begin(IsRoleValid, ClickRoleHandle, IsModifierValid, ClickModifierHandle); + + void ClickRoleHandle(RoleBehaviour role) + { + var realRole = player.Data.Role; + + var cachedMod = player.GetModifiers().FirstOrDefault(x => x is ICachedRole) as ICachedRole; + if (cachedMod != null) + { + realRole = cachedMod.CachedRole; + } + + var pickVictim = role.Role == realRole.Role; + var victim = pickVictim ? player : Player; + + ClickHandler(victim); + LastAttemptedVictim = player; + LastGuessedItem = $"{role.TeamColor.ToTextColor()}{role.NiceName}"; + } + + void ClickModifierHandle(BaseModifier modifier) + { + var pickVictim = player.HasModifier(modifier.TypeId); + var victim = pickVictim ? player : Player; + + ClickHandler(victim); + LastAttemptedVictim = player; + LastGuessedItem = + $"{MiscUtils.GetRoleColour(modifier.ModifierName.Replace(" ", string.Empty)).ToTextColor()}{modifier.ModifierName}"; + } + + void ClickHandler(PlayerControl victim) + { + if (victim != Player && victim.TryGetModifier(out var oracleMod)) + { + OracleRole.RpcOracleBlessNotify(oracleMod.Oracle, PlayerControl.LocalPlayer, victim); + + MeetingMenu.Instances.Do(x => x.HideSingle(victim.PlayerId)); + + shapeMenu.Close(); + LastGuessedItem = string.Empty; + LastAttemptedVictim = null; + + return; + } + + if (victim == Player && Player.TryGetModifier(out var modifier) && !modifier.Used) + { + 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); + + shapeMenu.Close(); + LastGuessedItem = string.Empty; + LastAttemptedVictim = null; + + return; + } + + Player.RpcCustomMurder(victim, createDeadBody: false, teleportMurderer: false, showKillAnim: false, + playKillSound: false); + + if (victim != Player) + { + LastGuessedItem = string.Empty; + LastAttemptedVictim = null; + 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; + } + } + + public bool IsExempt(PlayerVoteArea voteArea) + { + var votePlayer = voteArea.GetPlayer(); + return voteArea?.TargetPlayerId == Player.PlayerId || + Player.Data.IsDead || + voteArea!.AmDead || + (Player.IsImpostor() && votePlayer?.IsImpostor() == true && + !OptionGroupSingleton.Instance.FFAImpostorMode) || + (Player.Data.Role is VampireRole && votePlayer?.Data.Role is VampireRole) || + (votePlayer?.Data.Role is MayorRole mayor && mayor.Revealed) || + (votePlayer?.GetModifiers().Any(x => x.Visible && x.RevealRole) == true) || + (Player.IsLover() && votePlayer?.IsLover() == true) || + votePlayer?.HasModifier() == true; + } + + private bool IsRoleValid(RoleBehaviour role) + { + if (role.IsDead) + { + 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) + { + 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; + } + + private static bool IsModifierValid(BaseModifier modifier) + { + var isValid = true; + // This will remove modifiers that alter their chance/amount + if ((modifier is TouGameModifier touMod && (touMod.CustomAmount <= 0 || touMod.CustomChance <= 0)) || + (modifier is AllianceGameModifier allyMod && (allyMod.CustomAmount <= 0 || allyMod.CustomChance <= 0)) || + (modifier is UniversalGameModifier uniMod && (uniMod.CustomAmount <= 0 || uniMod.CustomChance <= 0))) + { + isValid = false; + } + + if (!isValid) + { + return false; + } + + if (OptionGroupSingleton.Instance.AssassinGuessAlliances && + modifier is AllianceGameModifier) + { + return true; + } + + if (!OptionGroupSingleton.Instance.AssassinGuessCrewModifiers) + { + return false; + } + + if (!OptionGroupSingleton.Instance.AssassinGuessUtilityModifiers && + modifier is TouGameModifier touMod2 && touMod2.FactionType == ModifierFaction.CrewmateUtility) + { + return false; + } + + var crewMod = modifier as TouGameModifier; + if (crewMod != null && crewMod.FactionType.ToDisplayString().Contains("Crew") && + !crewMod.FactionType.ToDisplayString().Contains("Non")) + { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/ChaosTokens/Modifiers/Effects/TokenBlind.cs b/ChaosTokens/Modifiers/Effects/TokenBlind.cs new file mode 100644 index 0000000..53a382b --- /dev/null +++ b/ChaosTokens/Modifiers/Effects/TokenBlind.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using HarmonyLib; +using MiraAPI.Modifiers; +using MiraAPI.Utilities; +using Reactor.Utilities; +using UnityEngine; + +namespace ChaosTokens.Modifiers.Effects; + +public class TokenBlind : TokenEffect +{ + public override ChaosEffects Effect => ChaosEffects.Blind; + public override string ModifierName => "Token Blind"; + public override string Notification => "You are blind. Partially"; + public override bool Negative => true; + public override bool RemoveOnDeath => true; + + private Color ogColor; + + public override void OnActivate() + { + base.OnActivate(); + var quad = HudManager.Instance.ShadowQuad; + ogColor = quad.material.GetColor(ShaderID.Color); + quad.material.SetColor(ShaderID.Color, Color.black); + + // Skeld + GameObject.Find("Hull2")?.SetActive(false); + GameObject.Find("HullItems")?.SetActive(false); + // Polus + GameObject.Find("Background")?.SetActive(false); + // Airship + GameObject.Find("engine_pipewheel")?.SetActive(false); + } + + public override void OnDeactivate() + { + base.OnDeactivate(); + var quad = HudManager.Instance.ShadowQuad; + quad.material.SetColor(ShaderID.Color, ogColor); + } +} \ No newline at end of file diff --git a/ChaosTokens/Modifiers/Effects/TokenScreenFlip.cs b/ChaosTokens/Modifiers/Effects/TokenScreenFlip.cs new file mode 100644 index 0000000..9429dcb --- /dev/null +++ b/ChaosTokens/Modifiers/Effects/TokenScreenFlip.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using HarmonyLib; +using MiraAPI.Modifiers; +using Reactor.Utilities; +using UnityEngine; + +namespace ChaosTokens.Modifiers.Effects; + +public class TokenScreenFlip : TokenEffect +{ + public override ChaosEffects Effect => ChaosEffects.ScreenFlip; + public override string ModifierName => "Token Screen Flip"; + public override string Notification => "Your screen is now flipped."; + public override bool Negative => true; + public override bool RemoveOnDeath => true; + + public override void OnActivate() + { + base.OnActivate(); + Coroutines.Start(CoFlipCamera(-1)); + } + + public override void OnDeactivate() + { + base.OnDeactivate(); + Coroutines.Start(CoFlipCamera(1)); + } + + public override void OnMeetingStart() + { + Player.RemoveModifier(this); + } + + public IEnumerator CoFlipCamera(float value) + { + var cam = Camera.main!; + var quad = HudManager.Instance.ShadowQuad; + var camStart = cam.transform.localScale; + var camGoal = cam.transform.localScale with { y = value }; + var quadStart = quad.transform.localScale; + var quadGoal = quad.transform.localScale with { y = -quad.transform.localScale.y }; + + for (float t = 0; t < 1; t += Time.deltaTime) + { + cam.transform.localScale = Vector3.Lerp(camStart, camGoal, Math.Clamp(t, 0, 1)); + quad.transform.localScale = Vector3.Lerp(quadStart, quadGoal, Math.Clamp(t, 0, 1)); + yield return new WaitForEndOfFrame(); + } + + var buttons = new List(); + var bp = HudManager.Instance.transform.FindChild("Buttons"); + buttons.AddRange(bp.FindChild("BottomRight").GetComponentsInChildren()); + buttons.AddRange(bp.FindChild("BottomLeft").GetComponentsInChildren()); + foreach (var button in buttons) + { + button.transform.localPosition = button.transform.localPosition with { y = button.transform.localPosition.y * value }; + } + } +} \ No newline at end of file diff --git a/ChaosTokens/Patches/VisionPatch.cs b/ChaosTokens/Patches/VisionPatch.cs index b198532..84bec96 100644 --- a/ChaosTokens/Patches/VisionPatch.cs +++ b/ChaosTokens/Patches/VisionPatch.cs @@ -19,6 +19,12 @@ public static void Postfix(ShipStatus __instance, NetworkedPlayerInfo player, re if (player != null) { var pc = MiscUtils.PlayerById(player.PlayerId); + if (pc == null) return; + + if (pc.HasModifier()) + { + __result *= 0.1f; + } if (pc.TryGetModifier(out var mod)) { __result *= mod.Multiplier; diff --git a/ChaosTokens/Resources/tokenshader-android.bundle b/ChaosTokens/Resources/tokenshader-android.bundle new file mode 100644 index 0000000..fb33dd2 Binary files /dev/null and b/ChaosTokens/Resources/tokenshader-android.bundle differ diff --git a/ChaosTokens/TokenEvents.cs b/ChaosTokens/TokenEvents.cs index 2be1413..b50c384 100644 --- a/ChaosTokens/TokenEvents.cs +++ b/ChaosTokens/TokenEvents.cs @@ -201,6 +201,16 @@ public static void BeforeMurderEventHandler(BeforeMurderEvent @event) var target = @event.Target; if (target.HasModifier()) { + if (source.HasModifier()) + { + return; + } + + if (source == target) + { + return; + } + @event.Cancel(); if (source.AmOwner) { diff --git a/ChaosTokens/libs/TownOfUsMira.dll b/ChaosTokens/libs/TownOfUsMira.dll index d672235..3d8d676 100644 Binary files a/ChaosTokens/libs/TownOfUsMira.dll and b/ChaosTokens/libs/TownOfUsMira.dll differ