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