diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b93150f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,81 @@
+name: NewMod CI
+
+on:
+ push:
+ branches:
+ - main
+ - dev
+ - au-2025.3.25
+ pull_request:
+ branches:
+ - main
+ - dev
+ - au-2025.3.25
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ BuildingInsideCI: true
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+ with:
+ submodules: true
+
+ - name: Cache Dependencies
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.nuget/packages
+ ~/.cache/bepinex
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+
+ - name: Setup .NET 8.0
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore NuGet Packages
+ run: dotnet restore NewMod/NewMod.csproj
+
+ - name: Build NewMod (Release)
+ run: dotnet build NewMod/NewMod.csproj --configuration Release --no-restore
+
+ - name: Build NewMod (Debug)
+ run: dotnet build NewMod/NewMod.csproj --configuration Debug --no-restore
+
+ - name: Upload NewMod DLL (Release)
+ uses: actions/upload-artifact@v4
+ with:
+ name: NewMod
+ path: NewMod/bin/Release/net6.0/NewMod.dll
+
+ - name: Upload NewMod DLL (Debug)
+ uses: actions/upload-artifact@v4
+ with:
+ name: NewMod-Debug
+ path: NewMod/bin/Debug/net6.0/NewMod.dll
+
+ release:
+ needs: build
+ runs-on: ubuntu-latest
+ if: github.ref == 'refs/heads/main'
+
+ steps:
+ - name: Download Release DLL
+ uses: actions/download-artifact@v4
+ with:
+ name: NewMod
+
+ - name: Publish GitHub Release
+ uses: softprops/action-gh-release@v1
+ with:
+ tag_name: v1.2.${{ github.run_number }}
+ files: NewMod/bin/Release/net6.0/NewMod.dll
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 670815b..0081692 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
bin/
obj/
+libs/*
References/
/packages/
riderModule.iml
diff --git a/NewMod.sln b/NewMod.sln
index d2bada0..e9228dc 100644
--- a/NewMod.sln
+++ b/NewMod.sln
@@ -1,5 +1,4 @@
-๏ปฟ
-Microsoft Visual Studio Solution File, Format Version 12.00
+๏ปฟMicrosoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
@@ -9,12 +8,15 @@ Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
+ ANDROID|Any CPU = ANDROID|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FC05DD49-CE3A-41F4-8452-37686FE23D0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC05DD49-CE3A-41F4-8452-37686FE23D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC05DD49-CE3A-41F4-8452-37686FE23D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC05DD49-CE3A-41F4-8452-37686FE23D0A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FC05DD49-CE3A-41F4-8452-37686FE23D0A}.ANDROID|Any CPU.ActiveCfg = ANDROID|Any CPU
+ {FC05DD49-CE3A-41F4-8452-37686FE23D0A}.ANDROID|Any CPU.Build.0 = ANDROID|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/NewMod/Buttons/Revenant/DoomAwakening.cs b/NewMod/Buttons/Revenant/DoomAwakening.cs
index 4e686ca..fedbb5b 100644
--- a/NewMod/Buttons/Revenant/DoomAwakening.cs
+++ b/NewMod/Buttons/Revenant/DoomAwakening.cs
@@ -20,7 +20,7 @@ public class DoomAwakening : CustomActionButton
///
/// The name displayed on the button.
///
- public override string Name => "Doom Awakening";
+ public override string Name => "";
///
/// Cooldown time for this ability, as configured in .
@@ -43,9 +43,9 @@ public class DoomAwakening : CustomActionButton
public override float EffectDuration => OptionGroupSingleton.Instance.DoomAwakeningDuration;
///
- /// The icon or sprite representing this button. Here, set to an empty sprite.
+ /// The icon or sprite representing this button.
///
- public override LoadableAsset Sprite => MiraAssets.Empty;
+ public override LoadableAsset Sprite => NewModAsset.DoomAwakeningButton;
///
/// Specifies whether this button is enabled for the specified role.
@@ -74,6 +74,7 @@ protected override void OnClick()
var player = PlayerControl.LocalPlayer;
Coroutines.Start(StartDoomAwakening(player));
}
+ public static List killedPlayers = new();
///
/// Executes the Doom Awakening effect, increasing speed, fading the screen, and killing nearby players.
@@ -85,6 +86,9 @@ public System.Collections.IEnumerator StartDoomAwakening(PlayerControl player)
float originalSpeed = player.MyPhysics.Speed;
player.MyPhysics.Speed *= 2f;
+ var clip = NewModAsset.DoomAwakeningSound.LoadAsset();
+ SoundManager.Instance.PlaySound(clip, true, 1f, null);
+
var fullScreen = HudManager.Instance.FullScreen;
fullScreen.color = new Color(1f, 0f, 0f, 0f);
fullScreen.gameObject.SetActive(true);
@@ -103,34 +107,33 @@ public System.Collections.IEnumerator StartDoomAwakening(PlayerControl player)
float duration = EffectDuration;
float timer = 0f;
int killCount = 0;
+
float ghostInterval = 0.2f;
float ghostTimer = 0f;
Queue ghosts = new Queue();
SpriteRenderer playerRenderer = player.cosmetics.normalBodySprite.BodySprite;
- // Doom Awakening loop
while (timer < duration)
{
timer += Time.deltaTime;
ghostTimer += Time.deltaTime;
- // Create a trailing ghost sprite
if (ghostTimer >= ghostInterval && player.MyPhysics.Speed > 0.01f)
{
ghostTimer = 0f;
- GameObject ghost = new GameObject("Revenant-Ghost");
+ GameObject ghost = new("Revenant-Ghost");
var ghostRenderer = ghost.AddComponent();
ghostRenderer.sprite = playerRenderer.sprite;
ghostRenderer.flipX = playerRenderer.flipX;
ghostRenderer.flipY = playerRenderer.flipY;
- ghostRenderer.material = new Material(playerRenderer.material);
+ ghostRenderer.sharedMaterial = playerRenderer.sharedMaterial;
PlayerMaterial.SetColors(player.Data.DefaultOutfit.ColorId, ghostRenderer);
ghostRenderer.sortingLayerID = playerRenderer.sortingLayerID;
ghostRenderer.sortingOrder = playerRenderer.sortingOrder + 1;
ghost.transform.position = player.transform.position;
ghost.transform.rotation = player.transform.rotation;
- ghost.transform.localScale = new Vector3(0.7f, 0.7f, 1f);
+ ghost.transform.localScale = player.transform.lossyScale;
Coroutines.Start(Utils.FadeAndDestroy(ghost, 1f));
ghosts.Enqueue(ghost);
@@ -143,11 +146,10 @@ public System.Collections.IEnumerator StartDoomAwakening(PlayerControl player)
Object.Destroy(oldGhost);
}
}
-
// Kill any nearby players
foreach (var target in PlayerControl.AllPlayerControls)
{
- if (target == player || target.Data.IsDead || target.Data.Disconnected || target.inVent)
+ if (target == player || target.Data.IsDead || target.Data.Disconnected || target.inVent || target.Data.Role.IsImpostor)
continue;
if (Vector2.Distance(player.GetTruePosition(), target.GetTruePosition()) < 1f)
@@ -160,10 +162,19 @@ public System.Collections.IEnumerator StartDoomAwakening(PlayerControl player)
showKillAnim: false,
playKillSound: true);
killCount++;
+ killedPlayers.Add(target);
+ }
+ if (target.AmOwner)
+ {
+ SoundManager.Instance.PlaySound(NewModAsset.DoomAwakeningEndSound.LoadAsset(), false, 1f, null);
}
}
yield return null;
}
+ if (killedPlayers.Count >= 3)
+ {
+ SoundManager.Instance.PlaySound(NewModAsset.DoomAwakeningEndSound.LoadAsset(), false, 1f, null);
+ }
// Fade out the red overlay
float fadeOutTime = 0.5f;
@@ -177,9 +188,11 @@ public System.Collections.IEnumerator StartDoomAwakening(PlayerControl player)
// Restore original speed and conclude
player.MyPhysics.Speed = originalSpeed;
+ SoundManager.Instance.StopSound(clip);
RV.StalkingStates.Remove(player.PlayerId);
Coroutines.Start(CoroutinesHelper.CoNotify("Doom Awakening ended."));
Helpers.CreateAndShowNotification($"Doom Awakening killed {killCount} players", Color.red, null, null);
+ killedPlayers.Clear();
}
}
}
diff --git a/NewMod/Buttons/Revenant/FeignDeathButton.cs b/NewMod/Buttons/Revenant/FeignDeathButton.cs
index 91a5dbb..af97c93 100644
--- a/NewMod/Buttons/Revenant/FeignDeathButton.cs
+++ b/NewMod/Buttons/Revenant/FeignDeathButton.cs
@@ -53,6 +53,14 @@ public override bool Enabled(RoleBehaviour role)
{
return role is Rev && !Rev.HasUsedFeignDeath;
}
+ ///
+ /// Checks if this button can be used
+ ///
+ /// True if base conditions are met and the player hasn't used Feign Death; otherwise, false.
+ public override bool CanUse()
+ {
+ return base.CanUse() && !Rev.HasUsedFeignDeath;
+ }
///
/// Invoked when the Feign Death button is clicked, starting the feign death coroutine.
diff --git a/NewMod/Buttons/Visionary/ShowScreenshotButton.cs b/NewMod/Buttons/Visionary/ShowScreenshotButton.cs
index 246bb6b..8e7ce90 100644
--- a/NewMod/Buttons/Visionary/ShowScreenshotButton.cs
+++ b/NewMod/Buttons/Visionary/ShowScreenshotButton.cs
@@ -18,7 +18,7 @@ public class ShowScreenshotButton : CustomActionButton
///
/// The name displayed on this button.
///
- public override string Name => "Show Screenshot";
+ public override string Name => "";
///
/// The cooldown time for this button, based on .
@@ -36,9 +36,9 @@ public class ShowScreenshotButton : CustomActionButton
public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxScreenshots;
///
- /// The sprite asset for this button. Here, set to an empty sprite.
+ /// The sprite asset for this button.
///
- public override LoadableAsset Sprite => MiraAssets.Empty;
+ public override LoadableAsset Sprite => NewModAsset.ShowScreenshotButton;
///
/// The on-screen location where the button will appear.
diff --git a/NewMod/CustomGameModes/RevivalRoyale.cs b/NewMod/CustomGameModes/RevivalRoyale.cs
index 3b8ef53..27cac7c 100644
--- a/NewMod/CustomGameModes/RevivalRoyale.cs
+++ b/NewMod/CustomGameModes/RevivalRoyale.cs
@@ -47,7 +47,11 @@ 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/DebugWindow.cs b/NewMod/DebugWindow.cs
index 8759174..071e283 100644
--- a/NewMod/DebugWindow.cs
+++ b/NewMod/DebugWindow.cs
@@ -1,5 +1,6 @@
using AmongUs.GameOptions;
-using Il2CppInterop.Runtime.Attributes;
+using System.Linq;
+using System;
using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
@@ -16,104 +17,139 @@
using Reactor.Utilities.Attributes;
using Reactor.Utilities.ImGui;
using UnityEngine;
+using UnityEngine.Events;
+using Il2CppInterop.Runtime.Attributes;
-namespace NewMod;
-
-[RegisterInIl2Cpp]
-public class DebugWindow(nint ptr) : MonoBehaviour(ptr)
+namespace NewMod
{
- [HideFromIl2Cpp]
- public bool EnableDebugger { get; set; } = false;
- public readonly DragWindow DebuggingWindow = new(new Rect(10, 10, 0, 0), "NewMod Debug Window", () =>
+ [RegisterInIl2Cpp]
+ public class DebugWindow(nint ptr) : MonoBehaviour(ptr)
{
- bool isFreeplay = AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay;
-
- if (GUILayout.Button("Become Explosive Modifier"))
- {
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcAddModifier();
- }
- if (GUILayout.Button("Remove Explosive Modifier"))
- {
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcRemoveModifier();
- }
- if (GUILayout.Button("Disable Collider"))
- {
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.Collider.enabled = false;
- }
- if (GUILayout.Button("Enable Collider"))
- {
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.Collider.enabled = true;
- }
- if (GUILayout.Button("Become Necromancer"))
+ [HideFromIl2Cpp]
+ public bool EnableDebugger { get; set; } = false;
+ public readonly DragWindow DebuggingWindow = new(new Rect(10, 10, 0, 0), "NewMod Debug Window", () =>
{
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
- }
- if (GUILayout.Button("Become DoubleAgent"))
- {
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
- }
- if (GUILayout.Button("Become EnergyThief"))
- {
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
- }
- if (GUILayout.Button("Become SpecialAgent"))
- {
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
- }
- if (GUILayout.Button("Force Start Game"))
- {
- if (GameOptionsManager.Instance.CurrentGameOptions.NumImpostors is 1) return;
- AmongUsClient.Instance.StartGame();
- }
- if (GUILayout.Button("Increases Uses by 3"))
- {
- var player = PlayerControl.LocalPlayer;
- if (player.Data.Role is NecromancerRole)
+ bool isFreeplay = AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay;
+
+ if (GUILayout.Button("Become Explosive Modifier"))
+ {
+ if (!isFreeplay) return;
+ PlayerControl.LocalPlayer.RpcAddModifier();
+ }
+ if (GUILayout.Button("Remove Explosive Modifier"))
+ {
+ if (!isFreeplay) return;
+ PlayerControl.LocalPlayer.RpcRemoveModifier();
+ }
+ if (GUILayout.Button("Disable Collider"))
+ {
+ if (!isFreeplay) return;
+ PlayerControl.LocalPlayer.Collider.enabled = false;
+ }
+ if (GUILayout.Button("Enable Collider"))
+ {
+ if (!isFreeplay) return;
+ PlayerControl.LocalPlayer.Collider.enabled = true;
+ }
+ if (GUILayout.Button("Become Necromancer"))
+ {
+ if (!isFreeplay) return;
+ PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ }
+ if (GUILayout.Button("Become DoubleAgent"))
+ {
+ if (!isFreeplay) return;
+ PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ }
+ if (GUILayout.Button("Become EnergyThief"))
+ {
+ if (!isFreeplay) return;
+ PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ }
+ if (GUILayout.Button("Become SpecialAgent"))
+ {
+ if (!isFreeplay) return;
+ PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ }
+ if (GUILayout.Button("Force Start Game"))
{
- CustomButtonSingleton.Instance.IncreaseUses(3);
+ if (GameOptionsManager.Instance.CurrentGameOptions.NumImpostors is 1) return;
+ AmongUsClient.Instance.StartGame();
}
- else if (player.Data.Role is EnergyThief)
+ if (GUILayout.Button("Increases Uses by 3"))
{
- CustomButtonSingleton.Instance.IncreaseUses(3);
+ var player = PlayerControl.LocalPlayer;
+ if (player.Data.Role is NecromancerRole)
+ {
+ CustomButtonSingleton.Instance.IncreaseUses(3);
+ }
+ else if (player.Data.Role is EnergyThief)
+ {
+ CustomButtonSingleton.Instance.IncreaseUses(3);
+ }
+ else if (player.Data.Role is SpecialAgent)
+ {
+ CustomButtonSingleton.Instance.IncreaseUses(3);
+ }
+ else if (player.Data.Role is Prankster)
+ {
+ CustomButtonSingleton.Instance.IncreaseUses(3);
+ }
+ else
+ {
+ CustomButtonSingleton.Instance.IncreaseUses(3);
+ CustomButtonSingleton.Instance.IncreaseUses(3);
+ }
}
- else if (player.Data.Role is SpecialAgent)
+ if (GUILayout.Button("Randomly Cast a Vote"))
{
- CustomButtonSingleton.Instance.IncreaseUses(3);
+ if (!MeetingHud.Instance) return;
+ var randPlayer = Utils.GetRandomPlayer(p => !p.Data.IsDead && !p.Data.Disconnected);
+ MeetingHud.Instance.CmdCastVote(PlayerControl.LocalPlayer.PlayerId, randPlayer.PlayerId);
}
- else if (player.Data.Role is Prankster)
+ GUILayout.Space(4);
+
+ GUILayout.Label("Overload button tests", GUI.skin.box);
+
+ if (GUILayout.Button("Test Overload Finale"))
{
- CustomButtonSingleton.Instance.IncreaseUses(3);
+ OverloadRole.UnlockFinalAbility();
}
- else
+ if (GUILayout.Button("Test Absorb"))
{
- CustomButtonSingleton.Instance.IncreaseUses(3);
- CustomButtonSingleton.Instance.IncreaseUses(3);
+ var prey = Utils.GetRandomPlayer(p =>
+ !p.Data.IsDead &&
+ !p.Data.Disconnected &&
+ p.PlayerId != PlayerControl.LocalPlayer.PlayerId);
+ if (prey != null)
+ {
+ if (prey.Data.Role is ICustomRole customRole)
+ {
+ Debug.Log("[Overload] Absorbing ability from custom role...");
+ }
+ else if (prey.Data.Role.Ability != null)
+ {
+ var btn = Instantiate(
+ HudManager.Instance.AbilityButton,
+ HudManager.Instance.AbilityButton.transform.parent);
+ btn.SetFromSettings(prey.Data.Role.Ability);
+ var pb = btn.GetComponent();
+ pb.OnClick.RemoveAllListeners();
+ pb.OnClick.AddListener((UnityAction)prey.Data.Role.UseAbility);
+ }
+ }
}
+ });
+ public void OnGUI()
+ {
+ if (EnableDebugger) DebuggingWindow.OnGUI();
}
- if (GUILayout.Button("Randomly Cast a Vote"))
+ public void Update()
{
- if (!MeetingHud.Instance) return;
-
- var randPlayer = Utils.GetRandomPlayer(p => !p.Data.IsDead && !p.Data.Disconnected);
- MeetingHud.Instance.CmdCastVote(PlayerControl.LocalPlayer.PlayerId, randPlayer.PlayerId);
+ if (Input.GetKey(KeyCode.F3))
+ {
+ EnableDebugger = !EnableDebugger;
+ }
}
- });
-
- public void OnGUI()
- {
- if (EnableDebugger) DebuggingWindow.OnGUI();
- }
- public void Update()
- {
- if (Input.GetKey(KeyCode.F3))
- EnableDebugger = !EnableDebugger;
}
-}
\ No newline at end of file
+}
diff --git a/NewMod/DiscordStatus.cs b/NewMod/DiscordStatus.cs
index 13e8b70..41d96c4 100644
--- a/NewMod/DiscordStatus.cs
+++ b/NewMod/DiscordStatus.cs
@@ -8,13 +8,13 @@ namespace NewMod
[HarmonyPatch(typeof(ActivityManager), nameof(ActivityManager.UpdateActivity))]
public static class DiscordPlayStatusPatch
{
- public static void Prefix([HarmonyArgument(0)] Activity activity)
+ public static void Postfix([HarmonyArgument(0)] Activity activity)
{
if (activity == null) return;
var isBeta = true;
- string details = $"New Mod v{NewMod.ModVersion}" + (isBeta ? " (Beta)" : "(dev)");
+ string details = $"NewMod v{NewMod.ModVersion}" + (isBeta ? " (Beta)" : "(dev)");
activity.Details = details;
diff --git a/NewMod/ModCompatibility.cs b/NewMod/ModCompatibility.cs
new file mode 100644
index 0000000..178a4de
--- /dev/null
+++ b/NewMod/ModCompatibility.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Reflection;
+using BepInEx.Unity.IL2CPP;
+using MiraAPI.PluginLoading;
+using MiraAPI.Roles;
+
+namespace NewMod
+{
+ public static class ModCompatibility
+ {
+ public const string LaunchpadReloaded_GUID = "dev.xtracube.launchpad";
+ public static bool IsLaunchpadLoaded()
+ {
+ return IL2CPPChainloader.Instance.Plugins.ContainsKey(LaunchpadReloaded_GUID);
+ }
+ public static bool LaunchpadLoaded(out Assembly asm)
+ {
+ asm = null;
+ if (!IL2CPPChainloader.Instance.Plugins.TryGetValue(LaunchpadReloaded_GUID, out var lp)) return false;
+ asm = lp.Instance.GetType().Assembly;
+ return asm != null;
+ }
+ public static void Initialize()
+ {
+ if (!IsLaunchpadLoaded()) return;
+
+ NewMod.Instance.Log.LogMessage("LaunchpadReloaded detected. Enabling compatibility...");
+ }
+ public static void DisableRole(string roleName, string pluginGuid)
+ {
+ var plugin = MiraPluginManager.GetPluginByGuid(pluginGuid);
+ if (plugin == null) return;
+
+ foreach (var kv in plugin.GetRoles())
+ {
+ var role = kv.Value;
+
+ if (role is ICustomRole customRole && customRole.RoleName.Equals(roleName, StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ var config = customRole.Configuration;
+ customRole.SetChance(0);
+ customRole.SetCount(0);
+ customRole.ParentMod.PluginConfig.Save();
+ return;
+ }
+ catch (Exception e)
+ {
+ NewMod.Instance.Log.LogError($"Failed to disable role '{roleName}': {e.Message}");
+ }
+ }
+ }
+ }
+ public static bool IsRoleActive(string roleName)
+ {
+ foreach (var roles in RoleManager.Instance.AllRoles)
+ {
+ CustomRoleManager.GetCustomRoleBehaviour(roles.Role, out var customRole);
+
+ if (customRole != null && customRole.RoleName.Equals(roleName, StringComparison.OrdinalIgnoreCase))
+ {
+ return customRole.GetChance() > 0 && customRole.GetCount() > 0;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/NewMod/Modifiers/ExplosiveModifier.cs b/NewMod/Modifiers/ExplosiveModifier.cs
index 4e216e3..87ec638 100644
--- a/NewMod/Modifiers/ExplosiveModifier.cs
+++ b/NewMod/Modifiers/ExplosiveModifier.cs
@@ -13,6 +13,7 @@ public class ExplosiveModifier : TimedModifier
public override string ModifierName => "Explosive";
public override bool HideOnUi => false;
public override bool AutoStart => true;
+ public override bool ShowInFreeplay => true;
public override float Duration => OptionGroupSingleton.Instance.Duration;
public override bool RemoveOnComplete => true;
private bool isFlashing = false;
@@ -20,7 +21,7 @@ public class ExplosiveModifier : TimedModifier
{
return Player.Data.Role.CanVent;
}
- public override string GetHudString()
+ public override string GetDescription()
{
return ModifierName + "\nif you die, all nearby players are killed";
}
diff --git a/NewMod/Modifiers/FalseFormModifier.cs b/NewMod/Modifiers/FalseFormModifier.cs
new file mode 100644
index 0000000..bd15097
--- /dev/null
+++ b/NewMod/Modifiers/FalseFormModifier.cs
@@ -0,0 +1,75 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Modifiers.Types;
+using MiraAPI.Utilities;
+using NewMod.Options.Modifiers;
+using UnityEngine;
+
+namespace NewMod.Modifiers
+{
+ public class FalseFormModifier : TimedModifier
+ {
+ public override string ModifierName => "FalseForm";
+ public override bool AutoStart =>
+ OptionGroupSingleton.Instance.EnableModifier;
+ public override float Duration =>
+ (int)OptionGroupSingleton.Instance.FalseFormDuration;
+ public override bool ShowInFreeplay => true;
+ public override bool HideOnUi => false;
+ public override bool RemoveOnComplete => true;
+ private float timer;
+ private AppearanceBackup oldAppearance;
+ public override void OnActivate()
+ {
+ oldAppearance = new AppearanceBackup
+ {
+ PlayerName = Player.Data.PlayerName,
+ HatId = Player.Data.DefaultOutfit.HatId,
+ SkinId = Player.Data.DefaultOutfit.SkinId,
+ PetId = Player.Data.DefaultOutfit.PetId,
+ ColorId = Player.Data.DefaultOutfit.ColorId
+ };
+ }
+ public override bool? CanVent()
+ {
+ return Player.Data.Role.CanVent;
+ }
+ public override string GetDescription()
+ {
+ return ModifierName
+ + $"\nYour appearance changes every {OptionGroupSingleton.Instance.FalseFormAppearanceTimer.Value} seconds.";
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+
+ timer += Time.fixedDeltaTime;
+
+ if (timer >= OptionGroupSingleton.Instance.FalseFormAppearanceTimer.Value)
+ {
+ Player.RpcSetName(Helpers.RandomString(5));
+ Player.RpcSetColor((byte)Random.Range(0, Palette.PlayerColors.Count));
+ Player.RpcSetHat(HatManager.Instance.AllHats[Random.Range(0, HatManager.Instance.allHats.Count)].ProductId);
+ Player.RpcSetSkin(HatManager.Instance.AllSkins[Random.Range(0, HatManager.Instance.allSkins.Count)].ProductId);
+ Player.RpcSetPet(HatManager.Instance.AllPets[Random.Range(0, HatManager.Instance.allPets.Count)].ProductId);
+ }
+ }
+ public override void OnDeactivate()
+ {
+ if (OptionGroupSingleton.Instance.RevertAppearance)
+ {
+ Player.RpcSetName(oldAppearance.PlayerName);
+ Player.RpcSetColor((byte)oldAppearance.ColorId);
+ Player.RpcSetHat(oldAppearance.HatId);
+ Player.RpcSetSkin(oldAppearance.SkinId);
+ Player.RpcSetPet(oldAppearance.PetId);
+ }
+ }
+ }
+ class AppearanceBackup
+ {
+ public string PlayerName;
+ public string HatId, SkinId, PetId;
+ public int ColorId;
+ }
+}
diff --git a/NewMod/Modifiers/StickyModifier.cs b/NewMod/Modifiers/StickyModifier.cs
new file mode 100644
index 0000000..cbdd27a
--- /dev/null
+++ b/NewMod/Modifiers/StickyModifier.cs
@@ -0,0 +1,96 @@
+using System.Collections;
+using System.Collections.Generic;
+using MiraAPI.GameOptions;
+using MiraAPI.Modifiers.Types;
+using NewMod.Options.Modifiers;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace NewMod.Modifiers
+{
+ public class StickyModifier : TimedModifier
+ {
+ public override string ModifierName => "Sticky";
+
+ public override bool AutoStart =>
+ OptionGroupSingleton.Instance.EnableModifier;
+
+ public override float Duration =>
+ (int)OptionGroupSingleton.Instance.StickyDuration;
+
+ public override bool HideOnUi => false;
+ public override bool ShowInFreeplay => true;
+ public override bool RemoveOnComplete => true;
+
+ public static List linkedPlayers = new();
+
+ public override bool? CanVent()
+ {
+ return Player.Data.Role.CanVent;
+ }
+
+ public override string GetDescription()
+ {
+ float distance = OptionGroupSingleton.Instance.StickyDistance.Value;
+ float duration = OptionGroupSingleton.Instance.StickyDuration.Value;
+
+ return $"{ModifierName}: Pulls nearby players within {distance} units for {duration} seconds.";
+ }
+
+ public override void FixedUpdate()
+ {
+ base.FixedUpdate();
+
+ if (!Player.CanMove) return;
+
+ foreach (var player in PlayerControl.AllPlayerControls)
+ {
+ if (player == Player || linkedPlayers.Contains(player)) continue;
+
+ float distance = OptionGroupSingleton.Instance.StickyDistance.Value;
+
+ if (Vector2.Distance(player.GetTruePosition(), Player.GetTruePosition()) < distance)
+ {
+ linkedPlayers.Add(player);
+ Coroutines.Start(CoFollowStickyPlayer(player));
+ }
+ }
+ }
+
+ public IEnumerator CoFollowStickyPlayer(PlayerControl player)
+ {
+ float duration = 5f;
+
+ var info = new StickyState
+ {
+ StickyOwner = Player,
+ LinkedPlayer = player,
+ velocity = Vector3.zero,
+ };
+
+ yield return HudManager.Instance.StartCoroutine(
+ Effects.Overlerp(duration, new System.Action((t) =>
+ {
+ Vector3 targetPos = info.LinkedPlayer.transform.position;
+ Vector3 currentPos = info.StickyOwner.transform.position;
+
+ info.LinkedPlayer.transform.position = Vector3.SmoothDamp(
+ targetPos,
+ currentPos,
+ ref info.velocity,
+ t
+ );
+ })
+ ));
+
+ linkedPlayers.Remove(player);
+ }
+ }
+
+ class StickyState
+ {
+ public PlayerControl StickyOwner;
+ public PlayerControl LinkedPlayer;
+ public Vector3 velocity;
+ }
+}
diff --git a/NewMod/NewMod.cs b/NewMod/NewMod.cs
index c1291b9..ac43ea2 100644
--- a/NewMod/NewMod.cs
+++ b/NewMod/NewMod.cs
@@ -16,12 +16,21 @@
using NewMod.Utilities;
using NewMod.Roles.ImpostorRoles;
using MiraAPI.Events.Vanilla.Gameplay;
+using NewMod.Roles.NeutralRoles;
+using MiraAPI.Roles;
+using System;
+using MiraAPI.Hud;
+using UnityEngine.Events;
+using NewMod.Options.Roles.OverloadOptions;
+using MiraAPI.Events;
+using NewMod.Patches.Compatibility;
namespace NewMod;
[BepInPlugin(Id, "NewMod", ModVersion)]
[BepInDependency(ReactorPlugin.Id)]
[BepInDependency(MiraApiPlugin.Id)]
+[BepInDependency(ModCompatibility.LaunchpadReloaded_GUID, BepInDependency.DependencyFlags.SoftDependency)]
[ReactorModFlags(Reactor.Networking.ModFlags.RequireOnAllClients)]
[BepInProcess("Among Us.exe")]
public partial class NewMod : BasePlugin, IMiraPlugin
@@ -31,7 +40,7 @@ public partial class NewMod : BasePlugin, IMiraPlugin
public Harmony Harmony { get; } = new Harmony(Id);
public static BasePlugin Instance;
public static Minigame minigame;
- public const string SupportedAmongUsVersion = "2024.11.26";
+ public const string SupportedAmongUsVersion = "2025.6.10";
public static ConfigEntry ShouldEnableBepInExConsole { get; set; }
public ConfigFile GetConfigFile() => Config;
public string OptionsTitleText => "NewMod";
@@ -42,10 +51,16 @@ public override void Load()
ReactorCredits.Register(ReactorCredits.AlwaysShow);
Harmony.PatchAll();
CheckVersionCompatibility();
- NewModEventHandler.RegisterAllEvents();
+ NewModEventHandler.RegisterEventsLogs();
+
+ if (ModCompatibility.IsLaunchpadLoaded())
+ {
+ Harmony.PatchAll(typeof(LaunchpadCompatibility));
+ Harmony.PatchAll(typeof(LaunchpadHackTextPatch));
+ }
ShouldEnableBepInExConsole = Config.Bind("NewMod", "Console", false, "Whether to enable BepInEx Console for debugging");
- Instance.Log.LogMessage($"Loaded Successfully NewMod v{ModVersion} With MiraAPI Version : {MiraApiPlugin.Version} with ID : {MiraApiPlugin.Id}");
if (!ShouldEnableBepInExConsole.Value) ConsoleManager.DetachConsole();
+ Instance.Log.LogMessage($"Loaded Successfully NewMod v{ModVersion} With MiraAPI Version : {MiraApiPlugin.Version} with ID : {MiraApiPlugin.Id}");
}
public static void CheckVersionCompatibility()
{
@@ -82,7 +97,7 @@ public static void InitializeKeyBinds()
var deadBodies = Helpers.GetNearestDeadBodies(PlayerControl.LocalPlayer.GetTruePosition(), 20f, Helpers.CreateFilter(Constants.NotShipMask));
if (deadBodies != null && deadBodies.Count > 0)
{
- var randomIndex = Random.Range(0, deadBodies.Count);
+ var randomIndex = UnityEngine.Random.Range(0, deadBodies.Count);
var randomBodyPosition = deadBodies[randomIndex].transform.position;
PlayerControl.LocalPlayer.NetTransform.RpcSnapTo(randomBodyPosition);
}
@@ -92,12 +107,47 @@ public static void InitializeKeyBinds()
}
}
}
+
+ [RegisterEvent]
public static void OnAfterMurder(AfterMurderEvent evt)
{
- PlayerControl source = evt.Source;
- PlayerControl target = evt.Target;
+ var source = evt.Source;
+ var target = evt.Target;
Utils.RecordOnKill(source, target);
+
+ if (target != OverloadRole.chosenPrey) return;
+
+ foreach (var pc in PlayerControl.AllPlayerControls.ToArray().Where(p => p.AmOwner && p.Data.Role is OverloadRole))
+ {
+ if (target.Data.Role is ICustomRole customRole)
+ {
+ // TODO: Awaiting appropriate event in MiraAPI to implement this functionality.
+ }
+ else if (target.Data.Role is not ICustomRole)
+ {
+ var btn = Object.Instantiate(
+ HudManager.Instance.AbilityButton,
+ HudManager.Instance.AbilityButton.transform.parent);
+ btn.SetFromSettings(target.Data.Role.Ability);
+ var pb = btn.GetComponent();
+ pb.OnClick.RemoveAllListeners();
+ pb.OnClick.AddListener((UnityAction)target.Data.Role.UseAbility);
+ }
+ }
+ OverloadRole.AbsorbedAbilityCount++;
+ Coroutines.Start(CoroutinesHelper.CoNotify($"Charge {OverloadRole.AbsorbedAbilityCount}/{OptionGroupSingleton.Instance.NeededCharge}"));
+ OverloadRole.chosenPrey = null;
+
+ if (OverloadRole.AbsorbedAbilityCount >= OptionGroupSingleton.Instance.NeededCharge)
+ {
+ OverloadRole.UnlockFinalAbility();
+ }
+ else
+ {
+ Coroutines.Start(OverloadRole.CoShowMenu(1f));
+ }
}
+
[HarmonyPatch(typeof(TaskPanelBehaviour), nameof(TaskPanelBehaviour.SetTaskText))]
public static class SetTaskTextPatch
{
diff --git a/NewMod/NewMod.csproj b/NewMod/NewMod.csproj
index 3bb975c..44df510 100644
--- a/NewMod/NewMod.csproj
+++ b/NewMod/NewMod.csproj
@@ -2,23 +2,34 @@
1.1.0
dev
- Among Us Mod
+ NewMod is a mod for Among Us that introduces a variety of new roles, unique abilities
CallofCreator
net6.0
latest
embedded
+ Debug;Release;ANDROID
+
+
+
+ TRACE;PC
+
+
+
+ $(RestoreSources);$(MSBuildProjectDirectory)\..\libs\Android
+ TRACE;ANDROID_BUILD
-
-
+
+
+
-
-
+
+
+
-
\ No newline at end of file
+
diff --git a/NewMod/NewModAsset.cs b/NewMod/NewModAsset.cs
index 63a344e..d5e1666 100644
--- a/NewMod/NewModAsset.cs
+++ b/NewMod/NewModAsset.cs
@@ -10,5 +10,9 @@ public static class NewModAsset
public static LoadableResourceAsset ModLogo { get; } = new("NewMod.Resources.Logo.png");
public static LoadableResourceAsset Camera { get; } = new("NewMod.Resources.cam.png");
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 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");
}
\ No newline at end of file
diff --git a/NewMod/NewModEndReasons.cs b/NewMod/NewModEndReasons.cs
index 291370a..d051a9e 100644
--- a/NewMod/NewModEndReasons.cs
+++ b/NewMod/NewModEndReasons.cs
@@ -6,6 +6,8 @@ public enum NewModEndReasons
DoubleAgentWin = 111,
PranksterWin = 112,
SpecialAgentWin = 113,
- TheVisionaryWin = 114
+ TheVisionaryWin = 114,
+ OverloadWin = 115,
+ EgoistWin = 116
}
}
\ No newline at end of file
diff --git a/NewMod/NewModEventHandler.cs b/NewMod/NewModEventHandler.cs
index a8772ca..6d64da1 100644
--- a/NewMod/NewModEventHandler.cs
+++ b/NewMod/NewModEventHandler.cs
@@ -1,32 +1,22 @@
using System.Collections.Generic;
-using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
-using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Events.Vanilla.Usables;
using NewMod.Patches;
using NewMod.Patches.Roles.Visionary;
-using NewMod.Roles.NeutralRoles;
namespace NewMod
{
public static class NewModEventHandler
{
- public static void RegisterAllEvents()
+ public static void RegisterEventsLogs()
{
- var registrations = new List();
-
- MiraEventManager.RegisterEventHandler(EndGamePatch.OnGameEnd, 1);
- registrations.Add($"{nameof(GameEndEvent)}: {nameof(EndGamePatch.OnGameEnd)}");
-
- MiraEventManager.RegisterEventHandler(VisionaryVentPatch.OnEnterVent);
- registrations.Add($"{nameof(EnterVentEvent)}: {nameof(VisionaryVentPatch.OnEnterVent)}");
-
- MiraEventManager.RegisterEventHandler(VisionaryMurderPatch.OnBeforeMurder);
- registrations.Add($"{nameof(BeforeMurderEvent)}: {nameof(VisionaryMurderPatch.OnBeforeMurder)}");
-
- MiraEventManager.RegisterEventHandler(NewMod.OnAfterMurder);
- registrations.Add($"{nameof(AfterMurderEvent)}: {nameof(NewMod.OnAfterMurder)}");
-
+ var registrations = new List
+ {
+ $"{nameof(GameEndEvent)}: {nameof(EndGamePatch.OnGameEnd)}",
+ $"{nameof(EnterVentEvent)}: {nameof(VisionaryVentPatch.OnEnterVent)}",
+ $"{nameof(BeforeMurderEvent)}: {nameof(VisionaryMurderPatch.OnBeforeMurder)}",
+ $"{nameof(AfterMurderEvent)}: {nameof(NewMod.OnAfterMurder)}"
+ };
NewMod.Instance.Log.LogInfo("Registered events: " + "\n" + string.Join(", ", registrations));
}
}
diff --git a/NewMod/Options/CompatibilityOptions.cs b/NewMod/Options/CompatibilityOptions.cs
new file mode 100644
index 0000000..7db18a3
--- /dev/null
+++ b/NewMod/Options/CompatibilityOptions.cs
@@ -0,0 +1,38 @@
+using System;
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.OptionTypes;
+
+namespace NewMod.Options;
+public class CompatibilityOptions : AbstractOptionGroup
+{
+ public override string GroupName => "Mod Compatibility";
+ public override Func GroupVisible => ModCompatibility.IsLaunchpadLoaded;
+ public ModdedToggleOption AllowRevenantHitmanCombo { get; } = new("Allow Revenant & Hitman in Same Match", false)
+ {
+ ChangedEvent = value =>
+ {
+ HudManager.Instance.ShowPopUp(value
+ ? "You enabled the Revenant & Hitman combo. This may break game balance!"
+ : "Revenant & Hitman combo disabled. Only one will be allowed per match.");
+ }
+ };
+ public ModdedEnumOption Compatibility { get; } = new("Mod Compatibility", ModPriority.PreferNewMod)
+ {
+ ChangedEvent = value =>
+ {
+ HudManager.Instance.ShowPopUp(
+ value switch
+ {
+ ModPriority.PreferNewMod => "You selected 'PreferNewMod'. Medic will be disabled.\n" +
+ "Switch to 'Prefer LaunchpadReloaded' to enable Medic and disable Necromancer.",
+ ModPriority.PreferLaunchpadReloaded => "You selected 'PreferLaunchpadReloaded'. Necromancer will be disabled.\n" +
+ "Switch to 'PreferNewMod' to enable Necromancer and disable Medic.",
+ });
+ }
+ };
+ public enum ModPriority
+ {
+ PreferNewMod,
+ PreferLaunchpadReloaded
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Options/GeneralOption.cs b/NewMod/Options/GeneralOption.cs
index dc1c4ad..fe4ead2 100644
--- a/NewMod/Options/GeneralOption.cs
+++ b/NewMod/Options/GeneralOption.cs
@@ -3,14 +3,13 @@
using MiraAPI.GameOptions.OptionTypes;
namespace NewMod.Options;
-
public class GeneralOption : AbstractOptionGroup
{
public override string GroupName => "NewMod Group";
-
+
[ModdedToggleOption("Enable Teleportation")]
public bool EnableTeleportation { get; set; } = true;
[ModdedToggleOption("Can Open Cams")]
- public bool CanOpenCams {get; set;} = true;
+ public bool CanOpenCams { get; set; } = true;
}
\ No newline at end of file
diff --git a/NewMod/Options/Modifiers/FalseFormModifierOptions.cs b/NewMod/Options/Modifiers/FalseFormModifierOptions.cs
new file mode 100644
index 0000000..78bab9f
--- /dev/null
+++ b/NewMod/Options/Modifiers/FalseFormModifierOptions.cs
@@ -0,0 +1,33 @@
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.OptionTypes;
+using MiraAPI.Utilities;
+using NewMod.Modifiers;
+
+namespace NewMod.Options.Modifiers
+{
+ public class FalseFormModifierOptions : AbstractOptionGroup
+ {
+ public override string GroupName => "FalseForm Settings";
+ public ModdedToggleOption EnableModifier { get; } = new("Enable FalseForm", true);
+ public ModdedNumberOption FalseFormDuration { get; } =
+ new(
+ "Duration of the FalseForm effect",
+ 20f,
+ min: 10f,
+ max: 30f,
+ increment: 1f,
+ suffixType: MiraNumberSuffixes.Seconds
+ );
+ public ModdedNumberOption FalseFormAppearanceTimer { get; } =
+ new(
+ "Appearance Change Delay",
+ 5f,
+ min: 1f,
+ max: 10f,
+ increment: 0.5f,
+ suffixType: MiraNumberSuffixes.Seconds
+ );
+ public ModdedToggleOption RevertAppearance { get; } =
+ new("Revert appearance after FalseForm ends", true);
+ }
+}
diff --git a/NewMod/Options/Modifiers/StickyModifierOptions.cs b/NewMod/Options/Modifiers/StickyModifierOptions.cs
new file mode 100644
index 0000000..e0410b4
--- /dev/null
+++ b/NewMod/Options/Modifiers/StickyModifierOptions.cs
@@ -0,0 +1,31 @@
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.OptionTypes;
+using MiraAPI.Utilities;
+using NewMod.Modifiers;
+
+namespace NewMod.Options.Modifiers
+{
+ public class StickyModifierOptions : AbstractOptionGroup
+ {
+ public override string GroupName => "Sticky Settings";
+ public ModdedToggleOption EnableModifier { get; } = new("Enable StickyModifier", true);
+ public ModdedNumberOption StickyDuration { get; } =
+ new(
+ "Duration of Sticky Effect",
+ 15f,
+ min: 10f,
+ max: 30f,
+ increment: 0.5f,
+ suffixType: MiraNumberSuffixes.Seconds
+ );
+ public ModdedNumberOption StickyDistance { get; } =
+ new(
+ "Distance to trigger stickiness",
+ 1f,
+ min: 1f,
+ max: 3f,
+ increment: 0.5f,
+ suffixType: MiraNumberSuffixes.None
+ );
+ }
+}
diff --git a/NewMod/Options/Roles/EgoistOptions/EgoistOptions.cs b/NewMod/Options/Roles/EgoistOptions/EgoistOptions.cs
new file mode 100644
index 0000000..5781ba2
--- /dev/null
+++ b/NewMod/Options/Roles/EgoistOptions/EgoistOptions.cs
@@ -0,0 +1,23 @@
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.Attributes;
+using MiraAPI.GameOptions.OptionTypes;
+using MiraAPI.Utilities;
+using NewMod.Roles.NeutralRoles;
+
+namespace NewMod.Options.Roles.EgoistOptions
+{
+ public class EgoistRoleOptions : AbstractOptionGroup
+ {
+ public override string GroupName => "Egoist Settings";
+
+ public ModdedNumberOption MinimumVotesToWin { get; } =
+ new(
+ "Minimum votes on Egoist to trigger win",
+ 3,
+ min: 1,
+ max: 10,
+ increment: 1,
+ suffixType: MiraNumberSuffixes.None
+ );
+ }
+}
diff --git a/NewMod/Options/Roles/OverloadOptions/OverloadOptions.cs b/NewMod/Options/Roles/OverloadOptions/OverloadOptions.cs
new file mode 100644
index 0000000..3d9ab7a
--- /dev/null
+++ b/NewMod/Options/Roles/OverloadOptions/OverloadOptions.cs
@@ -0,0 +1,18 @@
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.Attributes;
+using MiraAPI.GameOptions.OptionTypes;
+using NewMod.Roles.NeutralRoles;
+
+namespace NewMod.Options.Roles.OverloadOptions;
+
+public class OverloadOptions : AbstractOptionGroup
+{
+ public override string GroupName => "The Overload";
+
+ [ModdedNumberOption("Needed Charge", min:1, max:3)]
+ public float NeededCharge { get; set; } = 2f;
+
+ [ModdedNumberOption("Max Uses", min:1, max:2)]
+ public float MaxUses { get; set; } = 1f;
+
+}
\ No newline at end of file
diff --git a/NewMod/Patches/Compatibility/LaunchpadCompatibility.cs b/NewMod/Patches/Compatibility/LaunchpadCompatibility.cs
new file mode 100644
index 0000000..e58a5a7
--- /dev/null
+++ b/NewMod/Patches/Compatibility/LaunchpadCompatibility.cs
@@ -0,0 +1,60 @@
+using HarmonyLib;
+using NewMod.Roles.ImpostorRoles;
+using System.Reflection;
+using TMPro;
+using UnityEngine;
+
+namespace NewMod.Patches.Compatibility
+{
+ public static class LaunchpadCompatibility
+ {
+ static MethodBase TargetMethod()
+ {
+ if (!ModCompatibility.LaunchpadLoaded(out var asm) || asm == null)
+ return null;
+
+ var type = asm.GetType("LaunchpadReloaded.Modifiers.HackedModifier");
+ var method = type?.GetMethod("OnTimerComplete", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ return method;
+ }
+
+ static bool Prefix(object __instance)
+ {
+ var playerField = __instance.GetType().GetField("Player", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ if (playerField == null) return true;
+
+ var player = playerField.GetValue(__instance) as PlayerControl;
+
+ if (player != null && Revenant.FeignDeathStates.ContainsKey(player.PlayerId))
+ {
+ NewMod.Instance.Log.LogInfo($"Blocked Launchpad hack death on Revenant {player.Data.PlayerName}");
+ return false;
+ }
+ return true;
+ }
+ }
+ public static class LaunchpadHackTextPatch
+ {
+ static MethodBase TargetMethod()
+ {
+ if (!ModCompatibility.LaunchpadLoaded(out var asm) || asm == null)
+ return null;
+
+ var type = asm.GetType("LaunchpadReloaded.Modifiers.HackedModifier");
+ var method = type?.GetMethod("FixedUpdate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ return method;
+ }
+
+ static void Postfix(object __instance)
+ {
+ var player = __instance.GetType().GetField("Player", BindingFlags.Instance | BindingFlags.Public)?.GetValue(__instance) as PlayerControl;
+ var hackedText = __instance.GetType().GetField("_hackedText", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(__instance) as TextMeshPro;
+
+ if (player != null && hackedText != null && Revenant.FeignDeathStates.ContainsKey(player.PlayerId))
+ {
+ hackedText.SetText("");
+ Debug.Log($"hackedText: {hackedText.text}");
+ }
+ }
+ }
+}
diff --git a/NewMod/Patches/Compatibility/StartGamePatch.cs b/NewMod/Patches/Compatibility/StartGamePatch.cs
new file mode 100644
index 0000000..d88e8be
--- /dev/null
+++ b/NewMod/Patches/Compatibility/StartGamePatch.cs
@@ -0,0 +1,44 @@
+using HarmonyLib;
+using MiraAPI.GameOptions;
+using NewMod.Options;
+
+namespace NewMod.Patches.Compatibility
+{
+ [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.CoStartGame))]
+ public static class StartGamePatch
+ {
+ public static bool Prefix(AmongUsClient __instance)
+ {
+ if (!ModCompatibility.IsLaunchpadLoaded()) return true;
+
+ var settings = OptionGroupSingleton.Instance;
+
+ if (!settings.AllowRevenantHitmanCombo)
+ {
+ var hitman = ModCompatibility.IsRoleActive("Hitman");
+ var revenant = ModCompatibility.IsRoleActive("Revenant");
+
+ if (hitman && revenant)
+ {
+ if (settings.Compatibility == CompatibilityOptions.ModPriority.PreferNewMod)
+ {
+ ModCompatibility.DisableRole("Hitman", ModCompatibility.LaunchpadReloaded_GUID);
+ }
+ else
+ {
+ ModCompatibility.DisableRole("Revenant", NewMod.Id);
+ }
+ }
+ }
+ if (settings.Compatibility == CompatibilityOptions.ModPriority.PreferNewMod)
+ {
+ ModCompatibility.DisableRole("Medic", ModCompatibility.LaunchpadReloaded_GUID);
+ }
+ else
+ {
+ ModCompatibility.DisableRole("Necromancer", NewMod.Id);
+ }
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Patches/EndGamePatch.cs b/NewMod/Patches/EndGamePatch.cs
index 9d5638c..80c94a8 100644
--- a/NewMod/Patches/EndGamePatch.cs
+++ b/NewMod/Patches/EndGamePatch.cs
@@ -10,11 +10,13 @@
using NewMod.Utilities;
using NewMod.Options.Roles.SpecialAgentOptions;
using MiraAPI.GameOptions;
+using MiraAPI.Events;
namespace NewMod.Patches
{
public static class EndGamePatch
{
+ [RegisterEvent]
public static void OnGameEnd(GameEndEvent evt)
{
EndGameManager endGameManager = evt?.EndGameManager;
@@ -147,7 +149,7 @@ private static string GetRoleName(CachedPlayerData playerData, out Color roleCol
}
}
- private static RoleTypes GetRoleType() where T : RoleBehaviour
+ private static RoleTypes GetRoleType() where T : ICustomRole
{
ushort roleId = RoleId.Get();
return (RoleTypes)roleId;
@@ -175,7 +177,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)
diff --git a/NewMod/Patches/LogoPatch.cs b/NewMod/Patches/LogoPatch.cs
deleted file mode 100644
index acbbc7a..0000000
--- a/NewMod/Patches/LogoPatch.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using UnityEngine;
-using HarmonyLib;
-
-namespace NewMod.Patches
-{
- [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))]
- [HarmonyPriority(Priority.High)]
- public static class NewModLogoPatch
- {
- public static SpriteRenderer LogoSprite;
-
- [HarmonyPostfix]
- public static void StartPostfix(MainMenuManager __instance)
- {
- var newparent = __instance.gameModeButtons.transform.parent;
- var Logo = new GameObject("NewmodLogo");
- Logo.transform.parent = newparent;
- Logo.transform.localPosition = new(0f, -0.07f, 1f);
- LogoSprite = Logo.AddComponent();
- LogoSprite.sprite = NewModAsset.ModLogo.LoadAsset();
- }
- }
-}
diff --git a/NewMod/Patches/MainMenuPatch.cs b/NewMod/Patches/MainMenuPatch.cs
new file mode 100644
index 0000000..e4cbe8b
--- /dev/null
+++ b/NewMod/Patches/MainMenuPatch.cs
@@ -0,0 +1,25 @@
+using UnityEngine;
+using HarmonyLib;
+
+namespace NewMod.Patches
+{
+ [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))]
+ [HarmonyPriority(Priority.VeryHigh)]
+ public static class MainMenuPatch
+ {
+ public static SpriteRenderer LogoSprite;
+
+ [HarmonyPostfix]
+ public static void StartPostfix(MainMenuManager __instance)
+ {
+ var newparent = __instance.transform.FindChild("MainCanvas/MainPanel/RightPanel");
+ var Logo = new GameObject("NewModLogo");
+ Logo.transform.SetParent(newparent, false);
+ Logo.transform.localPosition = new(2.34f, -0.7136f, 1f);
+ LogoSprite = Logo.AddComponent();
+ LogoSprite.sprite = NewModAsset.ModLogo.LoadAsset();
+
+ ModCompatibility.Initialize();
+ }
+ }
+}
diff --git a/NewMod/Patches/Roles/MeetingHudPatch.cs b/NewMod/Patches/Roles/MeetingHudPatch.cs
index a4a420c..ad05f64 100644
--- a/NewMod/Patches/Roles/MeetingHudPatch.cs
+++ b/NewMod/Patches/Roles/MeetingHudPatch.cs
@@ -70,72 +70,6 @@ public static bool Prefix(MeetingHud __instance, byte reporter)
return false;
}
}
- [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.VotingComplete))]
- public static class MeetingHud_VotingComplete_Patch
- {
- public static void Postfix(MeetingHud __instance, MeetingHud.VoterState[] states, NetworkedPlayerInfo exiled, bool tie)
- {
- if (tie || exiled == null) return;
-
- var exiledPlayer = Utils.PlayerById(exiled.PlayerId);
- foreach (var overload in PlayerControl.AllPlayerControls.ToArray().Where(p => p.AmOwner && p.Data.Role is OverloadRole))
- {
- if (!(exiledPlayer.Data.Role is ICustomRole))
- {
- if (exiledPlayer.Data.Role.Ability == null)
- {
- Coroutines.Start(CoroutinesHelper.CoNotify("No ability to absorb from this player."));
- continue;
- }
- if (OverloadRole.AbsorbedAbilityCount >= 3)
- {
- Coroutines.Start(CoroutinesHelper.CoNotify("Maximum abilities absorbed."));
- continue;
- }
- OverloadRole.AbsorbedAbilityCount++;
- var role = exiledPlayer.Data.Role;
-
- var absorbedButton = Object.Instantiate(HudManager.Instance.AbilityButton, HudManager.Instance.AbilityButton.transform.parent);
- absorbedButton.SetFromSettings(role.Ability);
-
- var pb = absorbedButton.GetComponent();
- pb.OnClick.RemoveAllListeners();
- pb.OnClick.AddListener((UnityAction)role.UseAbility);
-
- Coroutines.Start(CoroutinesHelper.CoNotify(
- $"Ability absorbed from {exiledPlayer.Data.PlayerName}. Total absorbed: {OverloadRole.AbsorbedAbilityCount}"));
- }
- else
- {
- if (OverloadRole.AbsorbedAbilityCount >= 3)
- {
- Coroutines.Start(CoroutinesHelper.CoNotify("Maximum abilities absorbed."));
- continue;
- }
- OverloadRole.AbsorbedAbilityCount++;
- var customRole = (ICustomRole)exiledPlayer.Data.Role;
- var parentMod = customRole.ParentMod;
- Debug.Log(parentMod == null);
-
- var buttons = parentMod.GetButtons();
- Debug.Log(buttons.Count);
-
- var exiledButton = buttons.First();
- var newButton = Activator.CreateInstance(exiledButton.GetType()) as CustomActionButton;
- newButton.CreateButton(HudManager.Instance.AbilityButton.transform.parent);
- newButton.OverrideName(exiledButton.Name);
- newButton.OverrideSprite(exiledButton.Sprite.LoadAsset());
-
- var passive = newButton.Button.GetComponent();
- passive.OnClick.RemoveAllListeners();
- passive.OnClick.AddListener((UnityAction)newButton.ClickHandler);
-
- Coroutines.Start(CoroutinesHelper.CoNotify(
- $"Custom ability absorbed from {exiledPlayer.Data.PlayerName}. Total absorbed: {OverloadRole.AbsorbedAbilityCount}"));
- }
- }
- }
- }
}
}
}
diff --git a/NewMod/Patches/Roles/Visionary/VisionaryPatches.cs b/NewMod/Patches/Roles/Visionary/VisionaryPatches.cs
index a8fed7d..9542554 100644
--- a/NewMod/Patches/Roles/Visionary/VisionaryPatches.cs
+++ b/NewMod/Patches/Roles/Visionary/VisionaryPatches.cs
@@ -4,11 +4,13 @@
using NewMod.Utilities;
using MiraAPI.Utilities;
using Reactor.Utilities;
+using MiraAPI.Events;
namespace NewMod.Patches.Roles.Visionary
{
public static class VisionaryVentPatch
{
+ [RegisterEvent]
public static void OnEnterVent(EnterVentEvent evt)
{
PlayerControl player = evt.Player;
@@ -26,7 +28,7 @@ public static void OnEnterVent(EnterVentEvent evt)
}
}
[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcExitVent))]
- public static void Postfix(PlayerPhysics __instance, int id)
+ public static void Postfix(PlayerPhysics __instance, int ventId)
{
var chancePercentage = (int)(0.2f * 100);
if (Helpers.CheckChance(chancePercentage))
@@ -47,6 +49,7 @@ public static void Postfix(PlayerPhysics __instance, int id)
}
public static class VisionaryMurderPatch
{
+ [RegisterEvent]
public static void OnBeforeMurder(BeforeMurderEvent evt)
{
PlayerControl source = evt.Source;
diff --git a/NewMod/Patches/StatsPopupPatch.cs b/NewMod/Patches/StatsPopupPatch.cs
index 98d171b..d2c191f 100644
--- a/NewMod/Patches/StatsPopupPatch.cs
+++ b/NewMod/Patches/StatsPopupPatch.cs
@@ -6,40 +6,57 @@
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
{
public static class CustomStatsManager
{
- private static readonly string SavePath = Path.Combine(Application.persistentDataPath, "customStats.dat");
+ private static readonly string SavePath = Path.Combine(Application.consoleLogPath, "customStats.dat");
public static Dictionary CustomRoleWins = new();
+ public static bool _loaded = false;
public static void SaveCustomStats()
{
try
{
- using var writer = new BinaryWriter(File.Open(SavePath, FileMode.Create));
+ using var fs = new FileStream(
+ SavePath,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.Write
+ );
+ using var writer = new BinaryWriter(fs);
var allRoles = RoleManager.Instance.AllRoles;
writer.Write(allRoles.Count);
foreach (var role in allRoles)
{
- RoleTypes roleType = role.Role;
- int winCount = 0;
+ string key;
+ int wins;
if (role is ICustomRole customRole)
{
- string roleName = customRole.RoleName;
- winCount = CustomRoleWins.ContainsKey(roleName) ? CustomRoleWins[roleName] : 0;
- writer.Write(roleName);
+ key = customRole.RoleName;
+ wins = CustomRoleWins.TryGetValue(key, out var w) ? w : 0;
}
else
{
- winCount = (int)StatsManager.Instance.GetRoleWinCount(roleType);
- writer.Write(roleType.ToString());
+#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(winCount);
+ writer.Write(key);
+ writer.Write(wins);
}
}
catch (Exception ex)
@@ -49,56 +66,34 @@ public static void SaveCustomStats()
}
public static void LoadCustomStats()
{
- try
- {
- if (!File.Exists(SavePath))
- {
- return;
- }
+ if (_loaded) return;
- using var reader = new BinaryReader(File.Open(SavePath, FileMode.Open));
-
- int roleCount = reader.ReadInt32();
- for (int i = 0; i < roleCount; i++)
- {
- string roleIdentifier = reader.ReadString();
- int winCount = reader.ReadInt32();
-
- if (Enum.TryParse(roleIdentifier, out RoleTypes roleType))
- {
- SetVanillaRoleWinCount(roleType, winCount);
- }
- else
- {
- CustomRoleWins[roleIdentifier] = winCount;
- }
- }
- }
- catch (Exception ex)
- {
- NewMod.Instance.Log.LogError(ex.ToString());
- }
- }
- private static void SetVanillaRoleWinCount(RoleTypes role, int winCount)
- {
- try
+ if (!File.Exists(SavePath))
{
- FieldInfo statsField = typeof(StatsManager).GetField("stats", BindingFlags.NonPublic | BindingFlags.Instance);
- var statsInstance = statsField.GetValue(StatsManager.Instance) as StatsManager.Stats;
-
- FieldInfo roleWinsField = typeof(StatsManager.Stats).GetField("roleWins", BindingFlags.NonPublic | BindingFlags.Instance);
- var roleWinsDict = roleWinsField.GetValue(statsInstance) as Dictionary;
- roleWinsDict[role] = (uint)winCount;
+ return;
}
- catch (Exception ex)
+
+ using var fs = new FileStream(
+ SavePath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.ReadWrite
+ );
+ using var reader = new BinaryReader(fs);
+
+ int count = reader.ReadInt32();
+ for (int i = 0; i < count; i++)
{
- NewMod.Instance.Log.LogError(ex.ToString());
+ var key = reader.ReadString();
+ var wins = reader.ReadInt32();
+ CustomRoleWins[key] = wins;
}
+ _loaded = true;
}
-
public static void IncrementRoleWin(ICustomRole customRole)
{
string roleName = customRole.RoleName;
+
if (CustomRoleWins.ContainsKey(roleName))
{
CustomRoleWins[roleName]++;
@@ -110,11 +105,15 @@ public static void IncrementRoleWin(ICustomRole customRole)
}
public static int GetRoleWins(ICustomRole customRole)
{
- return CustomRoleWins.ContainsKey(customRole.RoleName) ? CustomRoleWins[customRole.RoleName] : 0;
+ return CustomRoleWins.TryGetValue(customRole.RoleName, out var w) ? w : 0;
}
}
+#if PC
+ [HarmonyPatch(typeof(PlayerStatsData), nameof(PlayerStatsData.SaveStats))]
+#else
[HarmonyPatch(typeof(StatsManager), nameof(StatsManager.SaveStats))]
+#endif
public class SaveStatsPatch
{
public static void Postfix()
@@ -123,15 +122,25 @@ public static void Postfix()
}
}
+#if PC
+ [HarmonyPatch(typeof(PlayerStatsData), nameof(PlayerStatsData.GetRoleStat))]
+#else
[HarmonyPatch(typeof(StatsManager), nameof(StatsManager.LoadStats))]
+#endif
public class LoadStatsPatch
{
- public static void Postfix()
+#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
{
@@ -157,19 +166,35 @@ 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)
+ {
+
+ StatID statID = entry.Key;
+ StringNames stringNames = entry.Value;
+
+ StatsPopup.AppendStat(stringBuilder, stringNames, DataManager.Player.Stats.GetStat(statID));
+
+ }
+#else
foreach (StringNames stringName in StatsPopup.RoleSpecificStatsToShow)
{
- StatsPopup.AppendStat(stringBuilder, stringName, StatsManager.Instance.GetStat(stringName));
+ StatsPopup.AppendStat(stringBuilder, stringName, StatsManager.Instance.GetStat(stringName));
}
+#endif
__instance.StatsText.text = stringBuilder.ToString();
return false;
}
}
-}
+}
\ No newline at end of file
diff --git a/NewMod/Resources/Logo.png b/NewMod/Resources/Logo.png
index adaa6cd..c7941e9 100644
Binary files a/NewMod/Resources/Logo.png and b/NewMod/Resources/Logo.png differ
diff --git a/NewMod/Resources/Sounds/evil_laugh.wav b/NewMod/Resources/Sounds/evil_laugh.wav
new file mode 100644
index 0000000..3d19420
Binary files /dev/null and b/NewMod/Resources/Sounds/evil_laugh.wav differ
diff --git a/NewMod/Resources/Sounds/gloomy_aura.wav b/NewMod/Resources/Sounds/gloomy_aura.wav
new file mode 100644
index 0000000..3cfe87b
Binary files /dev/null and b/NewMod/Resources/Sounds/gloomy_aura.wav differ
diff --git a/NewMod/Resources/doomawakening.png b/NewMod/Resources/doomawakening.png
new file mode 100644
index 0000000..285491b
Binary files /dev/null and b/NewMod/Resources/doomawakening.png differ
diff --git a/NewMod/Resources/showscreenshot.png b/NewMod/Resources/showscreenshot.png
new file mode 100644
index 0000000..1162ed4
Binary files /dev/null and b/NewMod/Resources/showscreenshot.png differ
diff --git a/NewMod/Roles/CrewmateRoles/Specialist.cs b/NewMod/Roles/CrewmateRoles/Specialist.cs
index aea405f..808352d 100644
--- a/NewMod/Roles/CrewmateRoles/Specialist.cs
+++ b/NewMod/Roles/CrewmateRoles/Specialist.cs
@@ -94,6 +94,10 @@ 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/ImpostorRoles/Necromancer.cs b/NewMod/Roles/ImpostorRoles/Necromancer.cs
index 6de8598..1d650b5 100644
--- a/NewMod/Roles/ImpostorRoles/Necromancer.cs
+++ b/NewMod/Roles/ImpostorRoles/Necromancer.cs
@@ -20,6 +20,6 @@ public class NecromancerRole : ImpostorRole, ICustomRole
{
Icon = MiraAssets.Empty,
OptionsScreenshot = NewModAsset.Banner,
- MaxRoleCount = 3
+ MaxRoleCount = 3,
};
}
diff --git a/NewMod/Roles/ImpostorRoles/Revenant.cs b/NewMod/Roles/ImpostorRoles/Revenant.cs
index 5d651a7..1d067f1 100644
--- a/NewMod/Roles/ImpostorRoles/Revenant.cs
+++ b/NewMod/Roles/ImpostorRoles/Revenant.cs
@@ -1,9 +1,12 @@
using System.Collections.Generic;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace NewMod.Roles.ImpostorRoles;
+
public class Revenant : ImpostorRole, ICustomRole
{
public string RoleName => "Revenant";
@@ -29,7 +32,7 @@ public class Revenant : ImpostorRole, ICustomRole
RoleHintType = RoleHintType.RoleTab
};
public static Dictionary FeignDeathStates = new Dictionary();
- public static bool HasUsedFeignDeath = false;
+ public static bool HasUsedFeignDeath = false;
public static Dictionary StalkingStates = new Dictionary();
public class FeignDeathInfo
{
@@ -37,4 +40,18 @@ public class FeignDeathInfo
public DeadBody DeadBody;
public bool Reported;
}
+
+ [RegisterEvent]
+ public static void OnPlayerExit(PlayerLeaveEvent evt)
+ {
+ if (FeignDeathStates.ContainsKey(evt.ClientData.Character.PlayerId))
+ {
+ FeignDeathStates.Remove(evt.ClientData.Character.PlayerId);
+ }
+ if (StalkingStates.ContainsKey(evt.ClientData.Character.PlayerId))
+ {
+ StalkingStates.Remove(evt.ClientData.Character.PlayerId);
+ }
+ HasUsedFeignDeath = false;
+ }
}
diff --git a/NewMod/Roles/NeutralRoles/Egoist.cs b/NewMod/Roles/NeutralRoles/Egoist.cs
new file mode 100644
index 0000000..f81f332
--- /dev/null
+++ b/NewMod/Roles/NeutralRoles/Egoist.cs
@@ -0,0 +1,87 @@
+using System.Linq;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Meeting;
+using MiraAPI.GameOptions;
+using MiraAPI.Networking;
+using MiraAPI.Roles;
+using MiraAPI.Utilities;
+using NewMod.Options.Roles.EgoistOptions;
+using UnityEngine;
+
+namespace NewMod.Roles.NeutralRoles
+{
+ public class EgoistRole : CrewmateRole, ICustomRole
+ {
+ public string RoleName => "Egoist";
+ public string RoleDescription => "Crave attention. Earn revenge.";
+ public string RoleLongDescription =>
+ "You are the Egoist, a chaotic neutral entity.\n\n"
+ + "Your goal is to be ejected โ if you are, and enough players vote for you, they die and you win.";
+ public Color RoleColor => new Color(0.8f, 0.3f, 0.6f, 1f);
+ public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
+ public RoleOptionsGroup RoleOptionsGroup => RoleOptionsGroup.Neutral;
+
+ public CustomRoleConfiguration Configuration =>
+ new(this)
+ {
+ AffectedByLightOnAirship = false,
+ CanGetKilled = true,
+ UseVanillaKillButton = false,
+ CanUseVent = false,
+ CanUseSabotage = false,
+ TasksCountForProgress = false,
+ ShowInFreeplay = true,
+ HideSettings = false,
+ MaxRoleCount = 1,
+ OptionsScreenshot = null,
+ Icon = null,
+ };
+
+ [RegisterEvent]
+ public static void OnEjection(EjectionEvent evt)
+ {
+ var egoist = PlayerControl
+ .AllPlayerControls.ToArray()
+ .FirstOrDefault(p => p.Data.Role is EgoistRole);
+ if (egoist == null)
+ return;
+
+ var ejected = evt.ExileController.initData.networkedPlayer.Object;
+ if (ejected != egoist)
+ return;
+
+ int minVotes = OptionGroupSingleton.Instance.MinimumVotesToWin;
+
+ var voters = PlayerControl
+ .AllPlayerControls.ToArray()
+ .Where(p =>
+ {
+ var voteData = p.GetVoteData();
+ return voteData != null && voteData.VotedFor(egoist.PlayerId);
+ })
+ .ToList();
+
+ if (voters.Count >= minVotes)
+ {
+ foreach (var p in voters)
+ {
+ egoist.RpcCustomMurder(
+ p,
+ didSucceed: true,
+ resetKillTimer: false,
+ createDeadBody: true,
+ teleportMurderer: false,
+ showKillAnim: false,
+ playKillSound: true
+ );
+ }
+ GameManager.Instance.RpcEndGame((GameOverReason)NewModEndReasons.EgoistWin, false);
+ }
+ }
+
+ public override bool DidWin(GameOverReason gameOverReason)
+ {
+ return gameOverReason == (GameOverReason)NewModEndReasons.EgoistWin;
+ }
+ }
+}
diff --git a/NewMod/Roles/NeutralRoles/Overload.cs b/NewMod/Roles/NeutralRoles/Overload.cs
index 66eae8a..08afa27 100644
--- a/NewMod/Roles/NeutralRoles/Overload.cs
+++ b/NewMod/Roles/NeutralRoles/Overload.cs
@@ -1,14 +1,12 @@
-using System;
-using System.Reflection;
-using System.Linq;
-using MiraAPI.Events.Vanilla.Meeting;
+using System.Collections;
using MiraAPI.Events;
-using MiraAPI.Hud;
using MiraAPI.Roles;
-using NewMod.Utilities;
using Reactor.Utilities;
using UnityEngine;
+using MiraAPI.Events.Vanilla.Gameplay;
+using MiraAPI.Hud;
using UnityEngine.Events;
+using NewMod.Utilities;
namespace NewMod.Roles.NeutralRoles;
public class OverloadRole : ImpostorRole, ICustomRole
@@ -20,6 +18,7 @@ public class OverloadRole : ImpostorRole, ICustomRole
public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
public RoleOptionsGroup RoleOptionsGroup { get; } = RoleOptionsGroup.Neutral;
public static int AbsorbedAbilityCount = 0;
+ public static PlayerControl chosenPrey;
public CustomRoleConfiguration Configuration => new(this)
{
AffectedByLightOnAirship = false,
@@ -33,4 +32,52 @@ public class OverloadRole : ImpostorRole, ICustomRole
OptionsScreenshot = null,
Icon = null,
};
+ [RegisterEvent]
+ public static void OnRoundStart(RoundStartEvent evt)
+ {
+ if (PlayerControl.LocalPlayer.Data.Role is not OverloadRole) return;
+
+ if (evt.TriggeredByIntro)
+ {
+ AbsorbedAbilityCount = 0;
+ chosenPrey = null;
+
+ Coroutines.Start(CoShowMenu(1f));
+ }
+ }
+ public static IEnumerator CoShowMenu(float delay)
+ {
+ yield return new WaitForSeconds(delay);
+
+ if (PlayerControl.LocalPlayer.AmOwner && PlayerControl.LocalPlayer.Data.Role is OverloadRole && chosenPrey == null)
+ {
+ CustomPlayerMenu menu = CustomPlayerMenu.Create();
+
+ menu.Begin(
+ player => !player.Data.IsDead && !player.Data.Disconnected && player.PlayerId != PlayerControl.LocalPlayer.PlayerId,
+ prey =>
+ {
+ chosenPrey = prey;
+ menu.Close();
+ Coroutines.Start(CoroutinesHelper.CoNotify($"Chosen prey: {prey?.Data.PlayerName}"));
+ });
+ }
+ yield return null;
+ }
+ public static void UnlockFinalAbility()
+ {
+ var btn = Instantiate(HudManager.Instance.AbilityButton, HudManager.Instance.AbilityButton.transform.parent);
+ var rect = btn.GetComponent();
+ rect.SetParent(HudManager.Instance.transform, false);
+ rect.anchorMin = new(0.5f, 0.5f);
+ rect.anchorMax = new(0.5f, 0.5f);
+ rect.pivot = new(0.5f, 0.5f);
+ rect.anchoredPosition = Vector2.zero;
+
+ btn.OverrideText("OVERLOAD");
+ btn.transform.SetAsLastSibling();
+ var passive = btn.GetComponent();
+ passive.OnClick.RemoveAllListeners();
+ passive.OnClick.AddListener((UnityAction)(() => GameManager.Instance.RpcEndGame((GameOverReason)NewModEndReasons.OverloadWin, false)));
+ }
}
diff --git a/NewMod/Roles/NeutralRoles/Prankster.cs b/NewMod/Roles/NeutralRoles/Prankster.cs
index e2d0e3f..bd9c777 100644
--- a/NewMod/Roles/NeutralRoles/Prankster.cs
+++ b/NewMod/Roles/NeutralRoles/Prankster.cs
@@ -7,8 +7,8 @@ namespace NewMod.Roles.NeutralRoles;
public class Prankster : CrewmateRole, ICustomRole
{
public string RoleName => "Prankster";
- public string RoleDescription => "Set up fake bodies to trick others. When reported, each fake body triggers a funny or deadly surprise for the reporter";
- public string RoleLongDescription => RoleDescription;
+ public string RoleDescription => "Set up fake bodies to trick others";
+ public string RoleLongDescription => "When reported, each fake body triggers a funny or deadly surprise for the reporter";
public Color RoleColor => new Color(1f, 0.55f, 0f);
public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
public RoleOptionsGroup RoleOptionsGroup { get; } = RoleOptionsGroup.Neutral;
diff --git a/NewMod/Utilities/Utils.cs b/NewMod/Utilities/Utils.cs
index cc853b4..b31fb8c 100644
--- a/NewMod/Utilities/Utils.cs
+++ b/NewMod/Utilities/Utils.cs
@@ -718,6 +718,7 @@ public static IEnumerator StartFeignDeath(PlayerControl player)
Reported = false,
};
Revenant.FeignDeathStates[player.PlayerId] = info;
+
Coroutines.Start(CoroutinesHelper.CoNotify("You are now feigning death.\nYou will be revived in 10 seconds if unreported."));
float timer = 10f;
diff --git a/README.md b/README.md
index f82cc95..274c5e0 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
NewMod is a mod for Among Us that introduces a variety of new roles, unique abilities, and custom game modes, offering players exciting new ways to enjoy the game.
-๐ฑ Android support coming soonโข!
+๐ฑ NewMod now supports Android!
@@ -26,6 +26,7 @@
- [โจ Features](#-features)
- [๐ Compatibility](#-compatibility)
- [๐ค Contributing](#-contributing)
+- [๐ฑ Android](#-android)
- [๐ฅ Credits](#-credits)
- [โ ๏ธ Disclaimer](#-disclaimer)
@@ -42,11 +43,7 @@
# ๐ฅ Installation
-1. **Download the latest version of NewMod** for your Among Us installation from [here](https://github.com/CallOfCreator/NewMod/releases/latest).
-2. Copy all contents of your *vanilla* Among Us folder to a new folder (this will be your modded Among Us).
-3. Extract all contents of `BepInEx.Unity.Il2CPP-win-x86_be.725.zip` into your modded Among Us folder.
-4. Download the latest version of **Reactor** and **MiraAPI**, then place them into the `BepInEx/plugins` folder.
-5. Launch the game with `Among Us.exe`. The first run may take 4-5 minutes as the mod initializes.
+### For installation instructions, please visit: https://newmod.up.railway.app
---
@@ -56,44 +53,19 @@
- **๐ฅ๏ธ Description:** Allows crewmates to access security cameras from anywhere.
- **๐ Strategic Use:** Monitor other players without needing to go to the security room.
-### **2. Necromancer Role**
- - **๐ฎ Description:** A special Impostor role that can revive a dead player to join the Impostors.
- - **๐จ Keybinds:** Press `F4` for teleport ability.
-
-### **3. Revival Royale GameMode** *(currently unavailable)*
- - **โ๏ธ Description:** Every player is a Necromancer, competing to revive bodies. The first to reach a set number of revivals wins.
-
-### **4. Energy Thief Role**
- - **๐ก Description:** Drains energy from others, making them weaker.
-
-### **5. Double Agent Role**
- - **๐ Description:** Completes tasks but can sabotage neutrals and impostors after task completion.
-
-### **6. Special Agent Role**
- - **๐๏ธ Description:** A neutral role that assigns missions to other players.
- - **๐ Mission Mechanics:** The assigned player must complete the mission or face penalties.
- - If the target fails the mission, the Special Agent loses a point toward their win condition.
- - **๐น Surveillance Option:** The Special Agent can monitor the target through a camera if enabled.
- - **๐ Win Condition:** Successful mission completions contribute to the Special Agentโs victory.
-
-### **7. Visionary Role**
-- **๐ธ Description:** A unique Crewmate role with the ability to take in-game screenshots to capture key moments like kills, venting, or suspicious activities.
-- **๐ฑ๏ธ How It Works:**
- - The Visionary can take multiple screenshots, but the limit depends on game settings.
- - If multiple screenshots are taken, the Visionary can open the in-game chat to see available screenshots (e.g., `/1`, `/2`) and select which one to display during a meeting.
-- **๐งฉ Strategy:** Use screenshots to catch impostors or prove innocence.
-
+### For role information, please visit: https://newmod.up.railway.app/roles
---
# ๐ Compatibility
-NewMod v1.1.0 is compatible with YanplaRoles, allowing for an enhanced experience with combined custom roles. Below is the supported version of YanplaRoles:
+NewMod is compatible with yanplaRoles and LaunchpadReloaded, allowing for an enhanced experience with combined custom roles. Below are the supported versions of both mods:
+
| Mod Name | Mod Version | GitHub Link |
|--------------|-------------|------------------------------------------------------|
-| YanplaRoles | v0.1.6+ | [Download](https://github.com/yanpla/yanplaRoles) |
+| yanplaRoles | v0.1.6+ | [Download](https://github.com/yanpla/yanplaRoles) |
+| LaunchpadReloaded | v0.3.4+ | [Download](https://github.com/All-Of-Us-Mods/LaunchpadReloaded) |
-For more information on YanplaRoles, visit their official [GitHub page](https://github.com/yanpla/yanplaRoles).
---
@@ -103,6 +75,14 @@ If youโd like to contribute, feel free to join and improve the project!
---
+# ๐ฑ Android
+
+NewMod now officially supports Android.
+Special thanks to [@xtracube](https://github.com/XtraCube) for providing access to the Android game lib, ensuring NewMod is fully compatible with **Starlight** at launch.
+For more information about Starlight, please visit: [https://discord.gg/FYYqJU2bvp](https://discord.gg/FYYqJU2bvp)
+
+---
+
# ๐ฅ Credits
- **MiraAPI**: [MiraAPI GitHub](https://github.com/All-Of-Us-Mods/MiraAPI) - Among Us modding API and utility library, with inspiration for the debug window and the derivation of the gold color from MiraAPIExample Mod.
@@ -110,6 +90,8 @@ If youโd like to contribute, feel free to join and improve the project!
- **TownOfUs-R**: - Portions of code (PlayerById, GetClosestBody) and asset (ReviveSprite) derived from [Town-Of-Us-R](https://github.com/eDonnes124/Town-Of-Us-R).
- **MoreGamemodes**: [MoreGamemodes](https://github.com/Rabek009/MoreGamemodes) - Derivation of IsActive and IsSabotage code.
- **yanplaRoles**: [yanplaRoles](https://github.com/yanpla/yanplaRoles) - Portions of code (SavePlayerRole, GetPlayerRolesHistory).
+- **EloySus**: [EloySus](https://github.com/EloySus) โ for all button sprites used in NewMod
+- **Pixabay**: [Pixabay](https://pixabay.com) - For sound effects used in NewMod
---
diff --git a/nuget.config b/nuget.config
index db78244..784b019 100644
--- a/nuget.config
+++ b/nuget.config
@@ -3,5 +3,6 @@
+
\ No newline at end of file