diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b93150f..ea6a6ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,12 +5,10 @@ on: branches: - main - dev - - au-2025.3.25 pull_request: branches: - main - dev - - au-2025.3.25 workflow_dispatch: jobs: @@ -49,6 +47,9 @@ jobs: - name: Build NewMod (Debug) run: dotnet build NewMod/NewMod.csproj --configuration Debug --no-restore + - name: Build NewMod Android (Debug) + run: dotnet build NewMod/NewMod.csproj --configuration ANDROID --no-restore + - name: Upload NewMod DLL (Release) uses: actions/upload-artifact@v4 with: @@ -61,6 +62,13 @@ jobs: name: NewMod-Debug path: NewMod/bin/Debug/net6.0/NewMod.dll + - name: Upload NewMod DLL (Android) + uses: actions/upload-artifact@v4 + with: + name: NewMod-Android + path: NewMod/bin/ANDROID/net6.0/NewMod.dll + + release: needs: build runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 0081692..4cf2515 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ bin/ obj/ -libs/* References/ +NewMod/Components /packages/ riderModule.iml .idea diff --git a/NewMod/Buttons/EnergyThief/DrainButton.cs b/NewMod/Buttons/EnergyThief/DrainButton.cs index 61000cd..7d3d00c 100644 --- a/NewMod/Buttons/EnergyThief/DrainButton.cs +++ b/NewMod/Buttons/EnergyThief/DrainButton.cs @@ -88,7 +88,10 @@ public override bool Enabled(RoleBehaviour role) /// protected override void OnClick() { + var clip = NewModAsset.DrainSound.LoadAsset(); + PendingEffectManager.AddPendingEffect(Target); + SoundManager.Instance.PlaySound(clip, false, 1f, null); Utils.RecordDrainCount(PlayerControl.LocalPlayer); diff --git a/NewMod/Buttons/Injector/InjectButton.cs b/NewMod/Buttons/Injector/InjectButton.cs new file mode 100644 index 0000000..b82bfb9 --- /dev/null +++ b/NewMod/Buttons/Injector/InjectButton.cs @@ -0,0 +1,90 @@ +using MiraAPI.GameOptions; +using MiraAPI.Hud; +using MiraAPI.Utilities.Assets; +using UnityEngine; +using NewMod.Roles.NeutralRoles; +using NewMod.Options.Roles.InjectorOptions; +using MiraAPI.Utilities; +using System; +using static NewMod.Utilities.Utils; + +namespace NewMod.Buttons.Injector +{ + /// + /// Represents the serum injection button for the Injector role. + /// Allows injecting a random serum into nearby players. + /// + public class InjectButton : CustomActionButton + { + /// + /// The name displayed on the button (if any). + /// + public override string Name => "Inject"; + + /// + /// Cooldown time between uses, configured via . + /// + public override float Cooldown => OptionGroupSingleton.Instance.SerumCooldown; + + /// + /// Maximum allowed injections, configured via . + /// + public override int MaxUses => OptionGroupSingleton.Instance.MaxSerumUses; + + /// + /// Effect duration — unused here since injection is instant. + /// + public override float EffectDuration => 0f; + + /// + /// Screen location of the button on the HUD. + /// + public override ButtonLocation Location => ButtonLocation.BottomLeft; + + /// + /// Sprite/icon displayed on the button. + /// + public override LoadableAsset Sprite => MiraAssets.Empty; + + /// + /// Returns the closest valid player target within range, + /// used by the Injector to determine who can be injected. + /// + /// The nearest PlayerControl instance, or null if none is in range. + public override PlayerControl GetTarget() + { + return PlayerControl.LocalPlayer.GetClosestPlayer(true, Distance, false); + } + + /// + /// Sets an outline around the target player to visually indicate interaction, + /// such as highlighting a valid injection target for the Injector role. + /// + /// True to show the outline; false to hide it. + public override void SetOutline(bool active) + { + Target?.cosmetics.SetOutline(active, new Il2CppSystem.Nullable(Palette.AcceptedGreen)); + } + + /// + /// Determines whether this button is available for the current role. + /// + /// The current player's role. + /// True only for the Injector role. + public override bool Enabled(RoleBehaviour role) + { + return role is InjectorRole; + } + + /// + /// Called when the button is clicked. Applies a serum to the closest valid target. + /// + protected override void OnClick() + { + var serumValues = Enum.GetValues(typeof(SerumType)); + SerumType randomSerum = (SerumType)serumValues.GetValue(UnityEngine.Random.Range(0, serumValues.Length)); + + RpcApplySerum(PlayerControl.LocalPlayer, Target, randomSerum); + } + } +} diff --git a/NewMod/CustomGameModes/RevivalRoyale.cs b/NewMod/CustomGameModes/RevivalRoyale.cs index 27cac7c..2fb9268 100644 --- a/NewMod/CustomGameModes/RevivalRoyale.cs +++ b/NewMod/CustomGameModes/RevivalRoyale.cs @@ -47,11 +47,8 @@ public override void HudUpdate(HudManager instance) ReviveCounter.text = $"Revive Count: {ReviveCount}"; if (ReviveCount >= 6) { - #if PC + GameManager.Instance.RpcEndGame(GameOverReason.ImpostorsByKill, true); - #else - GameManager.Instance.RpcEndGame(GameOverReason.ImpostorByKill, true); - #endif break; } } diff --git a/NewMod/CustomRPC.cs b/NewMod/CustomRPC.cs index cb282f8..66d7db3 100644 --- a/NewMod/CustomRPC.cs +++ b/NewMod/CustomRPC.cs @@ -1,4 +1,5 @@ namespace NewMod; + public enum CustomRPC { Revive, @@ -6,5 +7,6 @@ public enum CustomRPC FakeBody, AssignMission, MissionSuccess, - MissionFails + MissionFails, + ApplySerum } \ No newline at end of file diff --git a/NewMod/ModCompatibility.cs b/NewMod/ModCompatibility.cs index 178a4de..0a037e5 100644 --- a/NewMod/ModCompatibility.cs +++ b/NewMod/ModCompatibility.cs @@ -31,7 +31,7 @@ public static void DisableRole(string roleName, string pluginGuid) var plugin = MiraPluginManager.GetPluginByGuid(pluginGuid); if (plugin == null) return; - foreach (var kv in plugin.GetRoles()) + foreach (var kv in plugin.Roles) { var role = kv.Value; diff --git a/NewMod/NewMod.cs b/NewMod/NewMod.cs index 0e3b246..fe16d72 100644 --- a/NewMod/NewMod.cs +++ b/NewMod/NewMod.cs @@ -37,7 +37,7 @@ namespace NewMod; public partial class NewMod : BasePlugin, IMiraPlugin { public const string Id = "com.callofcreator.newmod"; - public const string ModVersion = "1.2.0"; + public const string ModVersion = "1.2.1"; public Harmony Harmony { get; } = new Harmony(Id); public static BasePlugin Instance; public static Minigame minigame; diff --git a/NewMod/NewMod.csproj b/NewMod/NewMod.csproj index 95490e4..7c8caf9 100644 --- a/NewMod/NewMod.csproj +++ b/NewMod/NewMod.csproj @@ -1,6 +1,6 @@ - 1.2.0 + 1.2.1 dev NewMod is a mod for Among Us that introduces a variety of new roles, unique abilities CallofCreator @@ -21,9 +21,9 @@ - + - + diff --git a/NewMod/NewModAsset.cs b/NewMod/NewModAsset.cs index d5e1666..7753ca5 100644 --- a/NewMod/NewModAsset.cs +++ b/NewMod/NewModAsset.cs @@ -1,18 +1,27 @@ using MiraAPI.Utilities.Assets; namespace NewMod; + public static class NewModAsset { + // Miscellaneous public static LoadableResourceAsset Banner { get; } = new("NewMod.Resources.optionImage.png"); - public static LoadableResourceAsset DeadBodySprite { get; } = new("NewMod.Resources.deadbody.png"); - public static LoadableResourceAsset NecromancerButton { get; } = new("NewMod.Resources.Revive2.png"); public static LoadableResourceAsset Arrow { get; } = new("NewMod.Resources.Arrow.png"); public static LoadableResourceAsset ModLogo { get; } = new("NewMod.Resources.Logo.png"); - public static LoadableResourceAsset Camera { get; } = new("NewMod.Resources.cam.png"); + + // Button icons public static LoadableResourceAsset SpecialAgentButton { get; } = new("NewMod.Resources.givemission.png"); public static LoadableResourceAsset ShowScreenshotButton { get; } = new("NewMod.Resources.showscreenshot.png"); public static LoadableResourceAsset DoomAwakeningButton { get; } = new("NewMod.Resources.doomawakening.png"); + public static LoadableResourceAsset NecromancerButton { get; } = new("NewMod.Resources.Revive2.png"); + public static LoadableResourceAsset DeadBodySprite { get; } = new("NewMod.Resources.deadbody.png"); + public static LoadableResourceAsset Camera { get; } = new("NewMod.Resources.cam.png"); + + // SFX public static LoadableAudioResourceAsset ReviveSound { get; } = new("NewMod.Resources.Sounds.revive.wav"); public static LoadableAudioResourceAsset DoomAwakeningSound { get; } = new("NewMod.Resources.Sounds.gloomy_aura.wav"); public static LoadableAudioResourceAsset DoomAwakeningEndSound { get; } = new("NewMod.Resources.Sounds.evil_laugh.wav"); + public static LoadableAudioResourceAsset DrainSound { get; } = new("NewMod.Resources.Sounds.drain_sound.wav"); + public static LoadableAudioResourceAsset FeignDeathSound { get; } = new("NewMod.Resources.Sounds.feign_death.wav"); + public static LoadableAudioResourceAsset VisionarySound { get; } = new("NewMod.Resources.Sounds.visionary_sound.wav"); } \ No newline at end of file diff --git a/NewMod/NewModEndReasons.cs b/NewMod/NewModEndReasons.cs index d051a9e..ed23a21 100644 --- a/NewMod/NewModEndReasons.cs +++ b/NewMod/NewModEndReasons.cs @@ -8,6 +8,7 @@ public enum NewModEndReasons SpecialAgentWin = 113, TheVisionaryWin = 114, OverloadWin = 115, - EgoistWin = 116 + EgoistWin = 116, + InjectorWin = 117 } } \ No newline at end of file diff --git a/NewMod/Options/Roles/InjectorOptions/InjectorOptions.cs b/NewMod/Options/Roles/InjectorOptions/InjectorOptions.cs new file mode 100644 index 0000000..a89036e --- /dev/null +++ b/NewMod/Options/Roles/InjectorOptions/InjectorOptions.cs @@ -0,0 +1,60 @@ +using MiraAPI.GameOptions; +using MiraAPI.GameOptions.Attributes; +using MiraAPI.GameOptions.OptionTypes; +using MiraAPI.Utilities; +using NewMod.Roles.NeutralRoles; +using UnityEngine; + +namespace NewMod.Options.Roles.InjectorOptions; + +public class InjectorOptions : AbstractOptionGroup +{ + public override string GroupName => "Injector Settings"; + + [ModdedNumberOption("Serum Cooldown", min: 5, max: 60, suffixType: MiraNumberSuffixes.Seconds)] + public float SerumCooldown { get; set; } = 20f; + + [ModdedNumberOption("Max Serum Uses", min: 1, max: 10)] + public int MaxSerumUses { get; set; } = 3; + + [ModdedNumberOption("Injections Required to Win", min: 1, max: 10)] + public int RequiredInjectCount { get; set; } = 3; + + [ModdedNumberOption("Adrenaline Effect (+% Speed)", min: 10, max: 200, increment: 5, suffixType: MiraNumberSuffixes.Percent)] + public float AdrenalineSpeedBoost { get; set; } = 10f; + + [ModdedNumberOption("Immobilize Duration", min: 1, max: 10, suffixType: MiraNumberSuffixes.Seconds)] + public float ParalysisDuration { get; set; } = 4f; + + [ModdedNumberOption("Bounce Force (Horizontal)", min: 0f, max: 2f, increment: 0.1f)] + public float BounceForceHorizontal { get; set; } = 0.5f; + + [ModdedNumberOption("Bounce Force (Vertical)", min: 0f, max: 2f, increment: 0.1f)] + public float BounceForceVertical { get; set; } = 0.5f; + + [ModdedToggleOption("Enable Random Bounce Effects")] + public bool EnableBounceVariants { get; set; } = true; + + [ModdedNumberOption("Bounce Duration", min: 1, max: 10, suffixType: MiraNumberSuffixes.Seconds)] + public float BounceDuration { get; set; } = 10f; + + public ModdedNumberOption BounceRotateEffect { get; } = new("Bounce Rotate Effect", 180f, min: 0f, max: 180f, increment: 10f, suffixType: MiraNumberSuffixes.None) + { + Visible = () => OptionGroupSingleton.Instance.EnableBounceVariants + }; + public ModdedNumberOption BounceStretchScale { get; } = new("Bounce Stretch Scale", 1.5f, min: 1f, max: 1.5f, increment: 0.01f, suffixType: MiraNumberSuffixes.Multiplier) + { + Visible = () => OptionGroupSingleton.Instance.EnableBounceVariants + }; + + [ModdedNumberOption("Repel Duration", min: 1, max: 10, suffixType: MiraNumberSuffixes.Seconds)] + public float RepelDuration { get; set; } = 10f; + + [ModdedNumberOption("Repel Range", min: 0.5f, max: 4f, increment: 0.1f)] + public float RepelRange { get; set; } = 2f; + + [ModdedNumberOption("Repel Force", min: 0.1f, max: 2f, increment: 0.1f, suffixType: MiraNumberSuffixes.Multiplier)] + public float RepelForce { get; set; } = 0.3f; +} + + diff --git a/NewMod/Patches/EndGamePatch.cs b/NewMod/Patches/EndGamePatch.cs index 80c94a8..45fe903 100644 --- a/NewMod/Patches/EndGamePatch.cs +++ b/NewMod/Patches/EndGamePatch.cs @@ -11,6 +11,7 @@ using NewMod.Options.Roles.SpecialAgentOptions; using MiraAPI.GameOptions; using MiraAPI.Events; +using NewMod.Options.Roles.InjectorOptions; namespace NewMod.Patches { @@ -177,7 +178,7 @@ private static Color GetRoleColor(RoleTypes roleType) } } - [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] + [HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))] public static class CheckGameEndPatch { public static bool Prefix(ShipStatus __instance) @@ -219,6 +220,12 @@ public static bool CheckEndGameForRole(ShipStatus __instance, GameOverReason int netScore = missionSuccessCount - missionFailureCount; shouldEndGame = netScore >= OptionGroupSingleton.Instance.RequiredMissionsToWin; } + if (typeof(T) == typeof(InjectorRole)) + { + int injectedCount = Utils.GetInjectedCount(); + int required = OptionGroupSingleton.Instance.RequiredInjectCount; + shouldEndGame = injectedCount >= required; + } if (shouldEndGame) { GameManager.Instance.RpcEndGame(winReason, false); diff --git a/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs b/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs index 4fa1237..f5fdc37 100644 --- a/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs +++ b/NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs @@ -13,6 +13,7 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)] EndGam Utils.ResetDrainCount(); Utils.ResetMissionSuccessCount(); Utils.ResetMissionFailureCount(); + Utils.ClearInjections(); PranksterUtilities.ResetReportCount(); VisionaryUtilities.DeleteAllScreenshots(); Revenant.HasUsedFeignDeath = false; diff --git a/NewMod/Patches/Roles/Visionary/VisionaryPatches.cs b/NewMod/Patches/Roles/Visionary/VisionaryPatches.cs index 9542554..d96d924 100644 --- a/NewMod/Patches/Roles/Visionary/VisionaryPatches.cs +++ b/NewMod/Patches/Roles/Visionary/VisionaryPatches.cs @@ -15,6 +15,7 @@ public static void OnEnterVent(EnterVentEvent evt) { PlayerControl player = evt.Player; var chancePercentage = (int)(0.2f * 100); + if (Helpers.CheckChance(chancePercentage)) { string timestamp = System.DateTime.UtcNow.ToString("yyyy-MM-dd_HH-mm-ss"); diff --git a/NewMod/Patches/StatsPopupPatch.cs b/NewMod/Patches/StatsPopupPatch.cs index d2c191f..f446d70 100644 --- a/NewMod/Patches/StatsPopupPatch.cs +++ b/NewMod/Patches/StatsPopupPatch.cs @@ -6,13 +6,9 @@ using System; using System.Collections.Generic; using System.IO; - -using System.Linq; -using System.Reflection; -#if PC using AmongUs.Data.Player; using AmongUs.Data; -#endif + namespace NewMod.Patches { @@ -48,12 +44,9 @@ public static void SaveCustomStats() } else { -#if PC + key = role.NiceName; wins = (int)DataManager.Player.Stats.GetRoleStat(role.Role, RoleStat.Wins); -#else - wins = (int)StatsManager.Instance.GetRoleWinCount(roleType); -#endif } writer.Write(key); writer.Write(wins); @@ -109,11 +102,8 @@ public static int GetRoleWins(ICustomRole customRole) } } -#if PC + [HarmonyPatch(typeof(PlayerStatsData), nameof(PlayerStatsData.SaveStats))] -#else - [HarmonyPatch(typeof(StatsManager), nameof(StatsManager.SaveStats))] -#endif public class SaveStatsPatch { public static void Postfix() @@ -122,31 +112,21 @@ public static void Postfix() } } -#if PC + [HarmonyPatch(typeof(PlayerStatsData), nameof(PlayerStatsData.GetRoleStat))] -#else - [HarmonyPatch(typeof(StatsManager), nameof(StatsManager.LoadStats))] -#endif public class LoadStatsPatch { -#if PC public static void Postfix(PlayerStatsData __instance, RoleTypes role, StatID stat) { CustomStatsManager.LoadCustomStats(); } -#else - public static void Postfix(StatsManager __instance) - { - CustomStatsManager.LoadCustomStats(); - } -#endif } [HarmonyPatch(typeof(StatsPopup), nameof(StatsPopup.DisplayRoleStats))] public class DisplayRoleStatsPatch { public static bool Prefix(StatsPopup __instance) { - StringBuilder stringBuilder = new StringBuilder(); + StringBuilder stringBuilder = new(); var allRoles = RoleManager.Instance.AllRoles; foreach (var role in allRoles) @@ -166,17 +146,13 @@ public static bool Prefix(StatsPopup __instance) { roleName = role.NiceName; roleColor = role.NameColor; -#if PC + winCount = (int)DataManager.Player.Stats.GetRoleStat(roleType, RoleStat.Wins); -#else - winCount = (int)StatsManager.Instance.GetRoleWinCount(roleType); -#endif } StatsPopup.AppendStat(stringBuilder, StringNames.StatsRoleWins, winCount, $"{roleName}"); } -#if PC foreach (var entry in StatsPopup.RoleSpecificStatsToShow) { @@ -186,12 +162,6 @@ public static bool Prefix(StatsPopup __instance) StatsPopup.AppendStat(stringBuilder, stringNames, DataManager.Player.Stats.GetStat(statID)); } -#else - foreach (StringNames stringName in StatsPopup.RoleSpecificStatsToShow) - { - StatsPopup.AppendStat(stringBuilder, stringName, StatsManager.Instance.GetStat(stringName)); - } -#endif __instance.StatsText.text = stringBuilder.ToString(); return false; diff --git a/NewMod/Resources/Sounds/drain_sound.wav b/NewMod/Resources/Sounds/drain_sound.wav new file mode 100644 index 0000000..260eaa7 Binary files /dev/null and b/NewMod/Resources/Sounds/drain_sound.wav differ diff --git a/NewMod/Resources/Sounds/feign_death.wav b/NewMod/Resources/Sounds/feign_death.wav new file mode 100644 index 0000000..51d8030 Binary files /dev/null and b/NewMod/Resources/Sounds/feign_death.wav differ diff --git a/NewMod/Resources/Sounds/visionary_sound.wav b/NewMod/Resources/Sounds/visionary_sound.wav new file mode 100644 index 0000000..9029e23 Binary files /dev/null and b/NewMod/Resources/Sounds/visionary_sound.wav differ diff --git a/NewMod/Roles/CrewmateRoles/Specialist.cs b/NewMod/Roles/CrewmateRoles/Specialist.cs index 808352d..87be4b7 100644 --- a/NewMod/Roles/CrewmateRoles/Specialist.cs +++ b/NewMod/Roles/CrewmateRoles/Specialist.cs @@ -94,10 +94,6 @@ public static void OnTaskComplete(CompleteTaskEvent evt) } public override bool DidWin(GameOverReason gameOverReason) { - #if PC return gameOverReason == GameOverReason.CrewmatesByTask; - #else - return gameOverReason == GameOverReason.HumansByTask; - #endif } } diff --git a/NewMod/Roles/NeutralRoles/Injector.cs b/NewMod/Roles/NeutralRoles/Injector.cs new file mode 100644 index 0000000..bdd4513 --- /dev/null +++ b/NewMod/Roles/NeutralRoles/Injector.cs @@ -0,0 +1,37 @@ +using MiraAPI.Roles; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace NewMod.Roles.NeutralRoles; + +public class InjectorRole : ImpostorRole, ICustomRole +{ + public string RoleName => "Injector"; + public string RoleDescription => "Inject other players with serums that alter their abilities"; + public string RoleLongDescription => "You hold unstable serums. Inject. Distort. Dominate"; + public Color RoleColor => new(0.9f, 0.3f, 0.1f); + public ModdedRoleTeams Team => ModdedRoleTeams.Custom; + public RoleOptionsGroup RoleOptionsGroup { get; } = RoleOptionsGroup.Neutral; + public CustomRoleConfiguration Configuration => new(this) + { + Icon = MiraAssets.Empty, + OptionsScreenshot = NewModAsset.Banner, + MaxRoleCount = 1, + UseVanillaKillButton = false, + CanUseVent = false, + TasksCountForProgress = false, + DefaultChance = 50, + DefaultRoleCount = 1, + CanModifyChance = true, + RoleHintType = RoleHintType.RoleTab + }; + public TeamIntroConfiguration TeamConfiguration => new() + { + IntroTeamDescription = RoleDescription, + IntroTeamColor = RoleColor + }; + public override bool DidWin(GameOverReason gameOverReason) + { + return gameOverReason == (GameOverReason)NewModEndReasons.InjectorWin; + } +} diff --git a/NewMod/Utilities/CoroutinesHelper.cs b/NewMod/Utilities/CoroutinesHelper.cs index 12a7ca9..ae3bcd6 100644 --- a/NewMod/Utilities/CoroutinesHelper.cs +++ b/NewMod/Utilities/CoroutinesHelper.cs @@ -355,5 +355,67 @@ public static IEnumerator CoHandleWantedTarget(ArrowBehaviour arrow, PlayerContr } yield break; } + /// + /// Resets the player's movement speed after the given delay. + /// Used to revert Adrenaline serum effect. + /// + /// The player whose speed will be reset. + /// The original speed value to restore. + /// The delay in seconds before restoring speed. + public static IEnumerator ResetSpeedAfterDelay(PlayerControl target, float originalSpeed, float delay) + { + yield return new WaitForSeconds(delay); + + if (target != null && !target.Data.IsDead) + { + target.MyPhysics.Speed = originalSpeed; + } + } + + /// + /// Enables movement for a player after a given delay. + /// Used to revert Paralysis serum effect. + /// + /// The player to re-enable movement for. + /// The delay in seconds before allowing movement. + public static IEnumerator EnableMovementAfterDelay(PlayerControl target, float delay) + { + yield return new WaitForSeconds(delay); + + if (target != null && !target.Data.IsDead) + { + target.moveable = true; + } + } + /// + /// Resets the player's rotation after a specified delay. + /// Useful for restoring normal orientation after bounce/spin effects (e.g. Bounce Serum). + /// + /// The player whose rotation will be reset. + /// The delay in seconds before resetting rotation. + public static IEnumerator ResetRotationAfterDelay(PlayerControl target, float delay) + { + yield return new WaitForSeconds(delay); + + if (target != null && !target.Data.IsDead) + { + target.transform.rotation = Quaternion.identity; + } + } + + /// + /// Resets any lingering repel-related effects on the target player after the Repel Serum expires. + /// + /// The player to reset after the repel effect. + /// The delay in seconds before performing the reset. + public static IEnumerator ResetRepelEffect(PlayerControl target, float delay) + { + yield return new WaitForSeconds(delay); + + if (!target.Data.IsDead && !target.Data.Disconnected) + { + target.MyPhysics.body.velocity = Vector2.zero; + } + } } } diff --git a/NewMod/Utilities/Utils.cs b/NewMod/Utilities/Utils.cs index 77a8fa9..7b5e25c 100644 --- a/NewMod/Utilities/Utils.cs +++ b/NewMod/Utilities/Utils.cs @@ -18,6 +18,8 @@ using NewMod.Roles.CrewmateRoles; using NewMod.Roles.ImpostorRoles; using NewMod.Roles.NeutralRoles; +using MiraAPI.GameOptions; +using NewMod.Options.Roles.InjectorOptions; namespace NewMod.Utilities { @@ -46,6 +48,12 @@ public static class Utils /// public static Dictionary MissionFailureCount = new Dictionary(); + /// + /// Stores the player IDs of all players who have been injected by the Injector role. + /// Used to track injection progress for win condition. + /// + public static readonly HashSet InjectedPlayerIds = new(); + /// /// Holds a set of players who are currently waiting for an event or action. /// @@ -345,6 +353,35 @@ public static void ResetMissionFailureCount() MissionFailureCount.Clear(); } + /// + /// Registers a player as having been injected by the Injector. + /// Adds the player's ID to the injected players tracking list. + /// + /// The player who was injected. + public static void RegisterPlayerInjection(PlayerControl target) + { + InjectedPlayerIds.Add(target.PlayerId); + } + + /// + /// Gets the number of unique players that have been injected by the Injector. + /// Used to evaluate the Injector's win condition. + /// + /// The total number of unique injected players. + public static int GetInjectedCount() + { + return InjectedPlayerIds.Count; + } + + + /// + /// Clear's InjectedPlayerIds at end of the game + /// + public static void ClearInjections() + { + InjectedPlayerIds.Clear(); + } + /// /// Sends an RPC to revive a player from a dead body. /// @@ -685,13 +722,17 @@ public static void RpcAssignMission(PlayerControl source, PlayerControl target) /// An IEnumerator for coroutine control. public static IEnumerator CaptureScreenshot(string filePath) { + var clip = NewModAsset.VisionarySound.LoadAsset(); + HudManager.Instance.SetHudActive(PlayerControl.LocalPlayer, PlayerControl.LocalPlayer.Data.Role, false); + SoundManager.Instance.PlaySound(clip, false, 1f, null); ScreenCapture.CaptureScreenshot(filePath, 4); VisionaryUtilities.CapturedScreenshotPaths.Add(filePath); NewMod.Instance.Log.LogInfo($"Capturing screenshot at {System.IO.Path.GetFileName(filePath)}."); yield return new WaitForSeconds(0.2f); + SoundManager.Instance.StopSound(clip); HudManager.Instance.SetHudActive(PlayerControl.LocalPlayer, PlayerControl.LocalPlayer.Data.Role, true); } @@ -702,6 +743,8 @@ public static IEnumerator CaptureScreenshot(string filePath) /// An IEnumerator for coroutine control. public static IEnumerator StartFeignDeath(PlayerControl player) { + var clip = NewModAsset.FeignDeathSound.LoadAsset(); + player.RpcCustomMurder(player, didSucceed: true, resetKillTimer: false, @@ -710,6 +753,9 @@ public static IEnumerator StartFeignDeath(PlayerControl player) showKillAnim: false, playKillSound: false); + + SoundManager.Instance.PlaySound(clip, false, 1f, null); + if (player.AmOwner) { HudManager.Instance.SetHudActive(false); @@ -738,6 +784,7 @@ public static IEnumerator StartFeignDeath(PlayerControl player) if (info.Reported) { yield return CoroutinesHelper.CoNotify("Your feign death has been reported. You remain dead."); + SoundManager.Instance.StopSound(clip); yield break; } } @@ -751,8 +798,9 @@ public static IEnumerator StartFeignDeath(PlayerControl player) if (player.AmOwner) { - DestroyableSingleton.Instance.SetHudActive(player, player.Data.Role, true); + HudManager.Instance.SetHudActive(player, player.Data.Role, true); } + SoundManager.Instance.StopSound(clip); } /// @@ -790,6 +838,112 @@ public static IEnumerator FadeAndDestroy(GameObject ghost, float fadeDuration) { typeof(SpecialAgent), new() { typeof(AssignButton) } }, { typeof(TheVisionary), new() { typeof(CaptureButton), typeof(ShowScreenshotButton) } } // TODO: Add Launchpad roles and their associated buttons here - }; + }; + + /// + /// Represents the different types of serums that the Injector role can apply to players. + /// Each serum causes a unique effect that alters gameplay. + /// + public enum SerumType + { + /// + /// Grants the target a burst of speed for a limited duration. + /// + Adrenaline, + + /// + /// Immobilizes the target, preventing them from moving for a short time. + /// + Paralysis, + + /// + /// Causes nearby players to be gently pushed away from the target for several seconds, + /// as if repelled by a magnetic force. + /// + RepelSerum, + + /// + /// Causes the target to bounce erratically for a few seconds. + /// + BounceSerum + + // More Coming Soon! + } + [MethodRpc((uint)CustomRPC.ApplySerum)] + + /// + /// Handles applying serum effects to target players for the Injector role. + /// + public static void RpcApplySerum(PlayerControl source, PlayerControl target, SerumType serumType) + { + switch (serumType) + { + case SerumType.Adrenaline: + { + float boostPercent = OptionGroupSingleton.Instance.AdrenalineSpeedBoost; + float multiplier = 1f + (boostPercent / 100f); + float originalSpeed = target.MyPhysics.Speed; + + target.MyPhysics.Speed *= multiplier; + + Coroutines.Start(CoroutinesHelper.ResetSpeedAfterDelay(target, originalSpeed, 10f)); + break; + } + + case SerumType.Paralysis: + { + float duration = OptionGroupSingleton.Instance.ParalysisDuration; + + target.MyPhysics.body.velocity = Vector2.zero; + + Coroutines.Start(CoroutinesHelper.EnableMovementAfterDelay(target, duration)); + break; + } + case SerumType.BounceSerum: + { + float bounceDuration = OptionGroupSingleton.Instance.BounceDuration; + float h = OptionGroupSingleton.Instance.BounceForceHorizontal; + float v = OptionGroupSingleton.Instance.BounceForceVertical; + float maxRotate = OptionGroupSingleton.Instance.BounceRotateEffect.Value; + + Vector2 force = new(Random.Range(-h, h), Random.Range(-v, v)); + + target.MyPhysics.body.AddForce(force); + + if (OptionGroupSingleton.Instance.EnableBounceVariants) + { + if (Helpers.CheckChance(OptionGroupSingleton.Instance.BounceRotateEffect)) + { + target.transform.Rotate(0, 0, Random.Range(-maxRotate, maxRotate)); + } + Coroutines.Start(CoroutinesHelper.ResetRotationAfterDelay(target, bounceDuration)); + } + } + break; + case SerumType.RepelSerum: + { + float RepelDuration = OptionGroupSingleton.Instance.RepelDuration; + float RepelRange = OptionGroupSingleton.Instance.RepelRange; + float RepelForce = OptionGroupSingleton.Instance.RepelForce; + + foreach (var other in PlayerControl.AllPlayerControls) + { + if (other == target || other.Data.IsDead && other.Data.Disconnected) continue; + + float dist = Vector2.Distance(other.GetTruePosition(), target.GetTruePosition()); + + if (dist < RepelRange) + { + Vector2 dir = (other.GetTruePosition() - target.GetTruePosition()).normalized; + other.MyPhysics.body.velocity += dir * RepelForce; + } + } + Coroutines.Start(CoroutinesHelper.ResetRepelEffect(target, RepelDuration)); + } + break; + } + RegisterPlayerInjection(target); + } } } + diff --git a/NewMod/Utilities/VisionaryUtilities.cs b/NewMod/Utilities/VisionaryUtilities.cs index 56dcb03..402fb73 100644 --- a/NewMod/Utilities/VisionaryUtilities.cs +++ b/NewMod/Utilities/VisionaryUtilities.cs @@ -84,8 +84,8 @@ public static IEnumerator ShowScreenshots(float displayDuration) var labelObj = new GameObject("Screenshot Label"); labelObj.transform.SetParent(screenshotPanel.transform, false); - var label = labelObj.AddComponent(); - label.alignment = TextAnchor.MiddleCenter; + var label = labelObj.AddComponent(); + label.alignment = TextAlignmentOptions.Center; label.fontSize = 20; DateTime captureTime = File.GetCreationTime(latestScreenshot); label.text = $"*Screenshot taken at: {captureTime.ToShortTimeString()}*"; @@ -167,8 +167,8 @@ public static IEnumerator ShowScreenshotByPath(string filePath, float displayDur var labelObj = new GameObject("Screenshot Label"); labelObj.transform.SetParent(screenshotPanel.transform, false); - var label = labelObj.AddComponent(); - label.alignment = TextAnchor.MiddleCenter; + var label = labelObj.AddComponent(); + label.alignment = TextAlignmentOptions.Center; label.fontSize = 20; DateTime captureTime = File.GetCreationTime(filePath); label.text = $"*Screenshot taken at: {captureTime.ToShortTimeString()}*"; diff --git a/libs/Android/AmongUs.GameLibs.Android.2025.6.10.nupkg b/libs/Android/AmongUs.GameLibs.Android.2025.6.10.nupkg new file mode 100644 index 0000000..7c62209 Binary files /dev/null and b/libs/Android/AmongUs.GameLibs.Android.2025.6.10.nupkg differ