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