Skip to content

Commit e0ff60e

Browse files
committed
Add Injector role and bump version to v1.2.1
1 parent f098a8b commit e0ff60e

9 files changed

Lines changed: 408 additions & 5 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using MiraAPI.GameOptions;
2+
using MiraAPI.Hud;
3+
using MiraAPI.Utilities.Assets;
4+
using UnityEngine;
5+
using NewMod.Roles.NeutralRoles;
6+
using NewMod.Options.Roles.InjectorOptions;
7+
using MiraAPI.Utilities;
8+
using System;
9+
using static NewMod.Utilities.Utils;
10+
11+
namespace NewMod.Buttons.Injector
12+
{
13+
/// <summary>
14+
/// Represents the serum injection button for the Injector role.
15+
/// Allows injecting a random serum into nearby players.
16+
/// </summary>
17+
public class InjectButton : CustomActionButton<PlayerControl>
18+
{
19+
/// <summary>
20+
/// The name displayed on the button (if any).
21+
/// </summary>
22+
public override string Name => "Inject";
23+
24+
/// <summary>
25+
/// Cooldown time between uses, configured via <see cref="InjectorOptions"/>.
26+
/// </summary>
27+
public override float Cooldown => OptionGroupSingleton<InjectorOptions>.Instance.SerumCooldown;
28+
29+
/// <summary>
30+
/// Maximum allowed injections, configured via <see cref="InjectorOptions"/>.
31+
/// </summary>
32+
public override int MaxUses => OptionGroupSingleton<InjectorOptions>.Instance.MaxSerumUses;
33+
34+
/// <summary>
35+
/// Effect duration — unused here since injection is instant.
36+
/// </summary>
37+
public override float EffectDuration => 0f;
38+
39+
/// <summary>
40+
/// Screen location of the button on the HUD.
41+
/// </summary>
42+
public override ButtonLocation Location => ButtonLocation.BottomLeft;
43+
44+
/// <summary>
45+
/// Sprite/icon displayed on the button.
46+
/// </summary>
47+
public override LoadableAsset<Sprite> Sprite => MiraAssets.Empty;
48+
49+
/// <summary>
50+
/// Returns the closest valid player target within range,
51+
/// used by the Injector to determine who can be injected.
52+
/// </summary>
53+
/// <returns>The nearest PlayerControl instance, or null if none is in range.</returns>
54+
public override PlayerControl GetTarget()
55+
{
56+
return PlayerControl.LocalPlayer.GetClosestPlayer(true, Distance, false);
57+
}
58+
59+
/// <summary>
60+
/// Sets an outline around the target player to visually indicate interaction,
61+
/// such as highlighting a valid injection target for the Injector role.
62+
/// </summary>
63+
/// <param name="active">True to show the outline; false to hide it.</param>
64+
public override void SetOutline(bool active)
65+
{
66+
Target?.cosmetics.SetOutline(active, new Il2CppSystem.Nullable<Color>(Palette.AcceptedGreen));
67+
}
68+
69+
/// <summary>
70+
/// Determines whether this button is available for the current role.
71+
/// </summary>
72+
/// <param name="role">The current player's role.</param>
73+
/// <returns>True only for the Injector role.</returns>
74+
public override bool Enabled(RoleBehaviour role)
75+
{
76+
return role is InjectorRole;
77+
}
78+
79+
/// <summary>
80+
/// Called when the button is clicked. Applies a serum to the closest valid target.
81+
/// </summary>
82+
protected override void OnClick()
83+
{
84+
var serumValues = Enum.GetValues(typeof(SerumType));
85+
SerumType randomSerum = (SerumType)serumValues.GetValue(UnityEngine.Random.Range(0, serumValues.Length));
86+
87+
RpcApplySerum(PlayerControl.LocalPlayer, Target, randomSerum);
88+
}
89+
}
90+
}

NewMod/CustomRPC.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
namespace NewMod;
2+
23
public enum CustomRPC
34
{
45
Revive,
56
Drain,
67
FakeBody,
78
AssignMission,
89
MissionSuccess,
9-
MissionFails
10+
MissionFails,
11+
ApplySerum
1012
}

NewMod/NewModEndReasons.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public enum NewModEndReasons
88
SpecialAgentWin = 113,
99
TheVisionaryWin = 114,
1010
OverloadWin = 115,
11-
EgoistWin = 116
11+
EgoistWin = 116,
12+
InjectorWin = 117
1213
}
1314
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using MiraAPI.GameOptions;
2+
using MiraAPI.GameOptions.Attributes;
3+
using MiraAPI.GameOptions.OptionTypes;
4+
using MiraAPI.Utilities;
5+
using NewMod.Roles.NeutralRoles;
6+
using UnityEngine;
7+
8+
namespace NewMod.Options.Roles.InjectorOptions;
9+
10+
public class InjectorOptions : AbstractOptionGroup<InjectorRole>
11+
{
12+
public override string GroupName => "Injector Settings";
13+
14+
[ModdedNumberOption("Serum Cooldown", min: 5, max: 60, suffixType: MiraNumberSuffixes.Seconds)]
15+
public float SerumCooldown { get; set; } = 20f;
16+
17+
[ModdedNumberOption("Max Serum Uses", min: 1, max: 10)]
18+
public int MaxSerumUses { get; set; } = 3;
19+
20+
[ModdedNumberOption("Injections Required to Win", min: 1, max: 10)]
21+
public int RequiredInjectCount { get; set; } = 3;
22+
23+
[ModdedNumberOption("Adrenaline Effect (+% Speed)", min: 10, max: 200, increment: 5, suffixType: MiraNumberSuffixes.Percent)]
24+
public float AdrenalineSpeedBoost { get; set; } = 10f;
25+
26+
[ModdedNumberOption("Immobilize Duration", min: 1, max: 10, suffixType: MiraNumberSuffixes.Seconds)]
27+
public float ParalysisDuration { get; set; } = 4f;
28+
29+
[ModdedNumberOption("Bounce Force (Horizontal)", min: 0f, max: 2f, increment: 0.1f)]
30+
public float BounceForceHorizontal { get; set; } = 0.5f;
31+
32+
[ModdedNumberOption("Bounce Force (Vertical)", min: 0f, max: 2f, increment: 0.1f)]
33+
public float BounceForceVertical { get; set; } = 0.5f;
34+
35+
[ModdedToggleOption("Enable Random Bounce Effects")]
36+
public bool EnableBounceVariants { get; set; } = true;
37+
38+
[ModdedNumberOption("Bounce Duration", min: 1, max: 10, suffixType: MiraNumberSuffixes.Seconds)]
39+
public float BounceDuration { get; set; } = 10f;
40+
41+
public ModdedNumberOption BounceRotateEffect { get; } = new("Bounce Rotate Effect", 180f, min: 0f, max: 180f, increment: 10f, suffixType: MiraNumberSuffixes.None)
42+
{
43+
Visible = () => OptionGroupSingleton<InjectorOptions>.Instance.EnableBounceVariants
44+
};
45+
public ModdedNumberOption BounceStretchScale { get; } = new("Bounce Stretch Scale", 1.5f, min: 1f, max: 1.5f, increment: 0.01f, suffixType: MiraNumberSuffixes.Multiplier)
46+
{
47+
Visible = () => OptionGroupSingleton<InjectorOptions>.Instance.EnableBounceVariants
48+
};
49+
50+
[ModdedNumberOption("Repel Duration", min: 1, max: 10, suffixType: MiraNumberSuffixes.Seconds)]
51+
public float RepelDuration { get; set; } = 10f;
52+
53+
[ModdedNumberOption("Repel Range", min: 0.5f, max: 4f, increment: 0.1f)]
54+
public float RepelRange { get; set; } = 2f;
55+
56+
[ModdedNumberOption("Repel Force", min: 0.1f, max: 2f, increment: 0.1f, suffixType: MiraNumberSuffixes.Multiplier)]
57+
public float RepelForce { get; set; } = 0.3f;
58+
}
59+
60+

NewMod/Patches/EndGamePatch.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using NewMod.Options.Roles.SpecialAgentOptions;
1212
using MiraAPI.GameOptions;
1313
using MiraAPI.Events;
14+
using NewMod.Options.Roles.InjectorOptions;
1415

1516
namespace NewMod.Patches
1617
{
@@ -177,7 +178,7 @@ private static Color GetRoleColor(RoleTypes roleType)
177178
}
178179
}
179180

180-
[HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))]
181+
[HarmonyPatch(typeof(LogicGameFlowNormal), nameof(LogicGameFlowNormal.CheckEndCriteria))]
181182
public static class CheckGameEndPatch
182183
{
183184
public static bool Prefix(ShipStatus __instance)
@@ -219,6 +220,12 @@ public static bool CheckEndGameForRole<T>(ShipStatus __instance, GameOverReason
219220
int netScore = missionSuccessCount - missionFailureCount;
220221
shouldEndGame = netScore >= OptionGroupSingleton<SpecialAgentOptions>.Instance.RequiredMissionsToWin;
221222
}
223+
if (typeof(T) == typeof(InjectorRole))
224+
{
225+
int injectedCount = Utils.GetInjectedCount();
226+
int required = OptionGroupSingleton<InjectorOptions>.Instance.RequiredInjectCount;
227+
shouldEndGame = injectedCount >= required;
228+
}
222229
if (shouldEndGame)
223230
{
224231
GameManager.Instance.RpcEndGame(winReason, false);

NewMod/Patches/Roles/EnergyThief/OnGameEnd.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)] EndGam
1313
Utils.ResetDrainCount();
1414
Utils.ResetMissionSuccessCount();
1515
Utils.ResetMissionFailureCount();
16+
Utils.ClearInjections();
1617
PranksterUtilities.ResetReportCount();
1718
VisionaryUtilities.DeleteAllScreenshots();
1819
Revenant.HasUsedFeignDeath = false;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using MiraAPI.Roles;
2+
using MiraAPI.Utilities.Assets;
3+
using UnityEngine;
4+
5+
namespace NewMod.Roles.NeutralRoles;
6+
7+
public class InjectorRole : ImpostorRole, ICustomRole
8+
{
9+
public string RoleName => "Injector";
10+
public string RoleDescription => "Inject other players with serums that alter their abilities";
11+
public string RoleLongDescription => "You hold unstable serums. Inject. Distort. Dominate";
12+
public Color RoleColor => new(0.9f, 0.3f, 0.1f);
13+
public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
14+
public RoleOptionsGroup RoleOptionsGroup { get; } = RoleOptionsGroup.Neutral;
15+
public CustomRoleConfiguration Configuration => new(this)
16+
{
17+
Icon = MiraAssets.Empty,
18+
OptionsScreenshot = NewModAsset.Banner,
19+
MaxRoleCount = 1,
20+
UseVanillaKillButton = false,
21+
CanUseVent = false,
22+
TasksCountForProgress = false,
23+
DefaultChance = 50,
24+
DefaultRoleCount = 1,
25+
CanModifyChance = true,
26+
RoleHintType = RoleHintType.RoleTab
27+
};
28+
public TeamIntroConfiguration TeamConfiguration => new()
29+
{
30+
IntroTeamDescription = RoleDescription,
31+
IntroTeamColor = RoleColor
32+
};
33+
public override bool DidWin(GameOverReason gameOverReason)
34+
{
35+
return gameOverReason == (GameOverReason)NewModEndReasons.InjectorWin;
36+
}
37+
}

NewMod/Utilities/CoroutinesHelper.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,5 +355,67 @@ public static IEnumerator CoHandleWantedTarget(ArrowBehaviour arrow, PlayerContr
355355
}
356356
yield break;
357357
}
358+
/// <summary>
359+
/// Resets the player's movement speed after the given delay.
360+
/// Used to revert Adrenaline serum effect.
361+
/// </summary>
362+
/// <param name="target">The player whose speed will be reset.</param>
363+
/// <param name="originalSpeed">The original speed value to restore.</param>
364+
/// <param name="delay">The delay in seconds before restoring speed.</param>
365+
public static IEnumerator ResetSpeedAfterDelay(PlayerControl target, float originalSpeed, float delay)
366+
{
367+
yield return new WaitForSeconds(delay);
368+
369+
if (target != null && !target.Data.IsDead)
370+
{
371+
target.MyPhysics.Speed = originalSpeed;
372+
}
373+
}
374+
375+
/// <summary>
376+
/// Enables movement for a player after a given delay.
377+
/// Used to revert Paralysis serum effect.
378+
/// </summary>
379+
/// <param name="target">The player to re-enable movement for.</param>
380+
/// <param name="delay">The delay in seconds before allowing movement.</param>
381+
public static IEnumerator EnableMovementAfterDelay(PlayerControl target, float delay)
382+
{
383+
yield return new WaitForSeconds(delay);
384+
385+
if (target != null && !target.Data.IsDead)
386+
{
387+
target.moveable = true;
388+
}
389+
}
390+
/// <summary>
391+
/// Resets the player's rotation after a specified delay.
392+
/// Useful for restoring normal orientation after bounce/spin effects (e.g. Bounce Serum).
393+
/// </summary>
394+
/// <param name="target">The player whose rotation will be reset.</param>
395+
/// <param name="delay">The delay in seconds before resetting rotation.</param>
396+
public static IEnumerator ResetRotationAfterDelay(PlayerControl target, float delay)
397+
{
398+
yield return new WaitForSeconds(delay);
399+
400+
if (target != null && !target.Data.IsDead)
401+
{
402+
target.transform.rotation = Quaternion.identity;
403+
}
404+
}
405+
406+
/// <summary>
407+
/// Resets any lingering repel-related effects on the target player after the Repel Serum expires.
408+
/// </summary>
409+
/// <param name="target">The player to reset after the repel effect.</param>
410+
/// <param name="delay">The delay in seconds before performing the reset.</param>
411+
public static IEnumerator ResetRepelEffect(PlayerControl target, float delay)
412+
{
413+
yield return new WaitForSeconds(delay);
414+
415+
if (!target.Data.IsDead && !target.Data.Disconnected)
416+
{
417+
target.MyPhysics.body.velocity = Vector2.zero;
418+
}
419+
}
358420
}
359421
}

0 commit comments

Comments
 (0)