diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ea6a6ee..1b19d53 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,13 +2,11 @@ name: NewMod CI
on:
push:
- branches:
- - main
- - dev
+ branches: [main, dev]
+ tags:
+ - 'v*'
pull_request:
- branches:
- - main
- - dev
+ branches: [main, dev]
workflow_dispatch:
jobs:
@@ -68,22 +66,24 @@ jobs:
name: NewMod-Android
path: NewMod/bin/ANDROID/net6.0/NewMod.dll
-
release:
needs: build
runs-on: ubuntu-latest
- if: github.ref == 'refs/heads/main'
+ if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
+ permissions:
+ contents: write
steps:
- name: Download Release DLL
uses: actions/download-artifact@v4
with:
name: NewMod
+ path: ./release_artifacts
- 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
+ tag_name: ${{ github.ref_name }}
+ files: ./release_artifacts/**/NewMod.dll
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/NewMod/Buttons/Aegis/AegisButton.cs b/NewMod/Buttons/Aegis/AegisButton.cs
new file mode 100644
index 0000000..cd8fc92
--- /dev/null
+++ b/NewMod/Buttons/Aegis/AegisButton.cs
@@ -0,0 +1,67 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.AegisOptions;
+using UnityEngine;
+using AG = NewMod.Roles.CrewmateRoles.Aegis;
+using NewMod.Utilities;
+using MiraAPI.Keybinds;
+
+namespace NewMod.Buttons.Aegis
+{
+ ///
+ /// Custom action button for the Aegis role. Places a configurable shield zone.
+ ///
+ public class AegisButton : CustomActionButton
+ {
+ ///
+ /// Gets the display name for this button.
+ ///
+ public override string Name => "Sentinel Ward";
+
+ ///
+ /// Cooldown is driven by AegisOptions.
+ ///
+ public override float Cooldown => OptionGroupSingleton.Instance.AegisCooldown;
+
+ ///
+ /// Maximum number of uses is driven by AegisOptions.
+ ///
+ public override int MaxUses => (int)OptionGroupSingleton.Instance.MaxCharges;
+
+ ///
+ /// Button location on HUD.
+ ///
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+
+ ///
+ /// Default keybind for Aegis.
+ ///
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+
+ ///
+ /// No “hold” effect, instant cast.
+ ///
+ public override float EffectDuration => 0f;
+
+ ///
+ /// Icon for the button (replace with your actual asset).
+ ///
+ public override LoadableAsset Sprite => NewModAsset.Shield;
+
+ ///
+ /// Enabled only for the Aegis role.
+ ///
+ public override bool Enabled(RoleBehaviour role) => role is AG;
+
+ ///
+ /// On click, place the Aegis shield at the player's current position using the configured settings.
+ ///
+ protected override void OnClick()
+ {
+ var player = PlayerControl.LocalPlayer;
+
+ AegisUtilities.ActivateShield(player, (Vector2)player.transform.position);
+ }
+ }
+}
diff --git a/NewMod/Buttons/Edgeveil/ArcButton.cs b/NewMod/Buttons/Edgeveil/ArcButton.cs
new file mode 100644
index 0000000..3414baa
--- /dev/null
+++ b/NewMod/Buttons/Edgeveil/ArcButton.cs
@@ -0,0 +1,92 @@
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Utilities.Assets;
+using NewMod.Options.Roles.EdgeveilOptions;
+using EV = NewMod.Roles.ImpostorRoles.Edgeveil;
+using Rewired;
+using UnityEngine;
+using NewMod.Components.ScreenEffects;
+using NewMod.Utilities;
+using Reactor.Utilities;
+using MiraAPI.Keybinds;
+
+namespace NewMod.Buttons.Edgeveil
+{
+ ///
+ /// Defines a custom action button for Edgeviel's Arc ability.
+ ///
+ public class ArcButton : CustomActionButton
+ {
+ ///
+ /// The name displayed on the button.
+ ///
+ public override string Name => "Arc";
+
+ ///
+ /// Gets the cooldown time for this button, based on .
+ ///
+ public override float Cooldown => OptionGroupSingleton.Instance.SlashCooldown;
+
+ ///
+ /// Gets the maximum number of uses for this button (0 = infinite).
+ ///
+ public override int MaxUses => (int)OptionGroupSingleton.Instance.SlashMaxUses;
+
+ ///
+ /// Determines how long the effect lasts. For Arc, none.
+ ///
+ public override float EffectDuration => 0f;
+
+ ///
+ /// Default keybind for Edgeveil's Arc ability.
+ ///
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
+
+ ///
+ /// Defines where on the screen this button should appear.
+ ///
+ public override ButtonLocation Location => ButtonLocation.BottomLeft;
+
+ ///
+ /// The visual icon for this button, set to the Edgeveil Arc sprite asset.
+ ///
+ public override LoadableAsset Sprite => NewModAsset.SlashIcon;
+
+ ///
+ /// Invoked when the Arc button is clicked.
+ ///
+ protected override void OnClick()
+ {
+ var player = PlayerControl.LocalPlayer;
+
+ bool flipLeft = player.cosmetics.currentBodySprite.BodySprite.flipX;
+ Vector2 dir = flipLeft ? Vector2.left : Vector2.right;
+
+ float spawnOffset = 0.55f;
+ var spawnPos = player.GetTruePosition() + dir * spawnOffset;
+
+ var tray = SlashTray.CreateTray();
+ tray.transform.SetParent(ShipStatus.Instance.transform, worldPositionStays: true);
+ tray.transform.SetPositionAndRotation(
+ new Vector3(spawnPos.x, spawnPos.y, player.transform.position.z),
+ Quaternion.FromToRotation(Vector3.right, new Vector3(dir.x, dir.y, 0f))
+ );
+
+ tray.Owner = player;
+ tray.SetMotion(dir, OptionGroupSingleton.Instance.SlashSpeed);
+
+ float effectDuration = OptionGroupSingleton.Instance.EffectDuration;
+
+ HudManager.Instance.PlayerCam.ShakeScreen(effectDuration, 2f);
+ }
+ ///
+ /// Determines whether this button is enabled for the role, returning true if the role is .
+ ///
+ /// The current player's role.
+ /// True if the role is Edgeveil; otherwise false.
+ public override bool Enabled(RoleBehaviour role)
+ {
+ return role is EV;
+ }
+ }
+}
diff --git a/NewMod/Buttons/EnergyThief/DrainButton.cs b/NewMod/Buttons/EnergyThief/DrainButton.cs
index 1e25552..0900a61 100644
--- a/NewMod/Buttons/EnergyThief/DrainButton.cs
+++ b/NewMod/Buttons/EnergyThief/DrainButton.cs
@@ -6,7 +6,7 @@
using ET = NewMod.Roles.NeutralRoles.EnergyThief;
using UnityEngine;
using NewMod.Utilities;
-using Rewired;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.EnergyThief
{
@@ -34,11 +34,11 @@ public class DrainButton : CustomActionButton
/// The on-screen position of this button.
///
public override ButtonLocation Location => ButtonLocation.BottomRight;
-
+
///
/// Default keybind for EnergyThief's Drain ability.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.F;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
///
/// The duration of the effect applied by this button; in this case, zero.
diff --git a/NewMod/Buttons/Injector/InjectButton.cs b/NewMod/Buttons/Injector/InjectButton.cs
index c20978c..63a4ba8 100644
--- a/NewMod/Buttons/Injector/InjectButton.cs
+++ b/NewMod/Buttons/Injector/InjectButton.cs
@@ -7,7 +7,7 @@
using MiraAPI.Utilities;
using System;
using static NewMod.Utilities.Utils;
-using Rewired;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.Injector
{
@@ -45,7 +45,7 @@ public class InjectButton : CustomActionButton
///
/// Default keybind for Injector's Inject ability.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.C;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
///
/// Sprite/icon displayed on the button.
diff --git a/NewMod/Buttons/Necromancer/ReviveButton.cs b/NewMod/Buttons/Necromancer/ReviveButton.cs
index cfb41df..e9b717b 100644
--- a/NewMod/Buttons/Necromancer/ReviveButton.cs
+++ b/NewMod/Buttons/Necromancer/ReviveButton.cs
@@ -5,7 +5,7 @@
using NewMod.Roles.ImpostorRoles;
using UnityEngine;
using NewMod.Utilities;
-using Rewired;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.Necromancer
{
@@ -37,7 +37,7 @@ public class ReviveButton : CustomActionButton
///
/// Default keybind for Necromancer's Revive ability.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.V;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
///
/// Defines where on the screen this button should appear.
diff --git a/NewMod/Buttons/Overload/FinalButton.cs b/NewMod/Buttons/Overload/FinalButton.cs
index 1e5d977..164512f 100644
--- a/NewMod/Buttons/Overload/FinalButton.cs
+++ b/NewMod/Buttons/Overload/FinalButton.cs
@@ -1,5 +1,6 @@
using MiraAPI.GameOptions;
using MiraAPI.Hud;
+using MiraAPI.Keybinds;
using MiraAPI.Utilities.Assets;
using NewMod.Options.Roles.OverloadOptions;
using NewMod.Roles.NeutralRoles;
@@ -32,7 +33,7 @@ public class FinalAbilityButton : CustomActionButton
///
/// Default keybind for the Final Ability button.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.B;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
///
/// No duration effect.
diff --git a/NewMod/Buttons/Overload/OverloadButton.cs b/NewMod/Buttons/Overload/OverloadButton.cs
index e478b02..c4266e2 100644
--- a/NewMod/Buttons/Overload/OverloadButton.cs
+++ b/NewMod/Buttons/Overload/OverloadButton.cs
@@ -1,7 +1,7 @@
using MiraAPI.Hud;
+using MiraAPI.Keybinds;
using MiraAPI.Utilities.Assets;
using NewMod.Roles.NeutralRoles;
-using Rewired;
using UnityEngine;
namespace NewMod.Buttons.Overload
@@ -47,7 +47,7 @@ public class OverloadButton : CustomActionButton
/// Stores the default key assigned to the absorbed button's action.
/// Mirrors the keybind of the original absorbed button.
///
- public KeyboardKeyCode absorbedKeybind;
+ public MiraKeybind absorbedKeybind;
///
/// The name displayed on the button.
@@ -67,7 +67,7 @@ public class OverloadButton : CustomActionButton
///
/// Default keybind for Overload's Overload ability.
///
- public override KeyboardKeyCode Defaultkeybind => absorbedKeybind;
+ public override MiraKeybind Keybind => absorbedKeybind;
///
/// Determines how long the effect from clicking the button lasts. In this case, no duration is set.
@@ -96,7 +96,7 @@ public void Absorb(CustomActionButton target)
absorbedCooldown = target.Cooldown;
absorbedMaxUses = target.MaxUses;
absorbedSprite = target.Sprite;
- absorbedKeybind = target.Defaultkeybind;
+ absorbedKeybind = target.Keybind;
absorbedOnClick = () => target.GetType().GetMethod("OnClick", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?.Invoke(target, null);
diff --git a/NewMod/Buttons/Prankster/FakeBodyButton.cs b/NewMod/Buttons/Prankster/FakeBodyButton.cs
index bb6db8e..1918908 100644
--- a/NewMod/Buttons/Prankster/FakeBodyButton.cs
+++ b/NewMod/Buttons/Prankster/FakeBodyButton.cs
@@ -7,6 +7,7 @@
using UnityEngine;
using NewMod.Utilities;
using Rewired;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.Prankster
{
@@ -38,7 +39,7 @@ public class FakeBodyButton : CustomActionButton
///
/// Default keybind for Prankster's Fake Body ability.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.Z;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
///
/// The duration of any effect caused by this button press; in this case, no effect duration is used.
diff --git a/NewMod/Buttons/PulseBlade/StrikeButton.cs b/NewMod/Buttons/PulseBlade/StrikeButton.cs
index 63dc186..7e56ecb 100644
--- a/NewMod/Buttons/PulseBlade/StrikeButton.cs
+++ b/NewMod/Buttons/PulseBlade/StrikeButton.cs
@@ -12,6 +12,7 @@
using NewMod.Utilities;
using Reactor.Networking.Attributes;
using Rewired;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.Pulseblade
{
@@ -48,10 +49,8 @@ public class StrikeButton : CustomActionButton
///
/// Default keybind for Pulseblade's Strike ability.
- /// Requires Shift as a modifier to prevent accidental use.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.G;
- public override ModifierKey Modifier1 => ModifierKey.Shift;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
///
/// Sprite used for the button — set to empty;
///
diff --git a/NewMod/Buttons/Revenant/DoomAwakening.cs b/NewMod/Buttons/Revenant/DoomAwakening.cs
index e3d50aa..8e5789d 100644
--- a/NewMod/Buttons/Revenant/DoomAwakening.cs
+++ b/NewMod/Buttons/Revenant/DoomAwakening.cs
@@ -9,8 +9,7 @@
using NewMod.Utilities;
using Reactor.Utilities;
using UnityEngine;
-using TMPro;
-using Rewired;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.Revenant
{
@@ -41,10 +40,9 @@ public class DoomAwakening : CustomActionButton
///
/// Default keybind for Doom's Awakening ability.
- /// Requires Alt as a modifier to prevent accidental use.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.B;
- public override ModifierKey Modifier1 => ModifierKey.Alt;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
+
///
/// Determines how long the effect lasts. Configured in .
///
diff --git a/NewMod/Buttons/Revenant/FeignDeathButton.cs b/NewMod/Buttons/Revenant/FeignDeathButton.cs
index ba58f1f..f0f0c87 100644
--- a/NewMod/Buttons/Revenant/FeignDeathButton.cs
+++ b/NewMod/Buttons/Revenant/FeignDeathButton.cs
@@ -6,7 +6,7 @@
using NewMod.Utilities;
using Reactor.Utilities;
using UnityEngine;
-using Rewired;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.Revenant
{
@@ -37,10 +37,8 @@ public class FeignDeathButton : CustomActionButton
///
/// Default keybind for Revenant's Feign Death ability.
- /// Requires Ctrl as a modifier to prevent accidental use.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.T;
- public override ModifierKey Modifier1 => ModifierKey.Control;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
///
/// The duration of any effect from this button. In this case, zero.
diff --git a/NewMod/Buttons/SpecialAgent/AssignButton.cs b/NewMod/Buttons/SpecialAgent/AssignButton.cs
index 06bb3cd..d080ef3 100644
--- a/NewMod/Buttons/SpecialAgent/AssignButton.cs
+++ b/NewMod/Buttons/SpecialAgent/AssignButton.cs
@@ -7,7 +7,7 @@
using UnityEngine;
using NewMod.Utilities;
using Reactor.Utilities;
-using Rewired;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.SpecialAgent
{
@@ -39,7 +39,7 @@ public class AssignButton : CustomActionButton
///
/// Default keybind for Special Agent's Assign ability.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.H;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
///
/// The duration of any effect triggered by this button; here, it's zero.
diff --git a/NewMod/Buttons/Visionary/CaptureButton.cs b/NewMod/Buttons/Visionary/CaptureButton.cs
index 9972408..02967ee 100644
--- a/NewMod/Buttons/Visionary/CaptureButton.cs
+++ b/NewMod/Buttons/Visionary/CaptureButton.cs
@@ -7,7 +7,7 @@
using UnityEngine;
using Reactor.Utilities;
using NewMod.Utilities;
-using Rewired;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.Visionary
{
@@ -48,7 +48,7 @@ public class CaptureButton : CustomActionButton
///
/// Default keybind for Visionary's Capture ability.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.N;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
///
/// Handles the button click, capturing a screenshot and saving it to a unique path.
diff --git a/NewMod/Buttons/Visionary/ShowScreenshotButton.cs b/NewMod/Buttons/Visionary/ShowScreenshotButton.cs
index 3d1222f..c8fa7c4 100644
--- a/NewMod/Buttons/Visionary/ShowScreenshotButton.cs
+++ b/NewMod/Buttons/Visionary/ShowScreenshotButton.cs
@@ -7,8 +7,8 @@
using NewMod.Utilities;
using Reactor.Utilities;
using System.Linq;
-using Rewired;
using System.IO;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.Visionary
{
@@ -50,7 +50,7 @@ public class ShowScreenshotButton : CustomActionButton
///
/// Default keybind for Visionary's Show ability.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.M;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
///
/// Checks if the button can be used, ensuring there's at least one captured screenshot.
diff --git a/NewMod/Buttons/WraithCaller/CallWraith.cs b/NewMod/Buttons/WraithCaller/CallWraith.cs
index 4ff742b..7a1031f 100644
--- a/NewMod/Buttons/WraithCaller/CallWraith.cs
+++ b/NewMod/Buttons/WraithCaller/CallWraith.cs
@@ -5,9 +5,9 @@
using Wraith = NewMod.Roles.NeutralRoles.WraithCaller;
using UnityEngine;
using NewMod.Utilities;
-using Rewired;
using System.Collections.Generic;
using System.Linq;
+using MiraAPI.Keybinds;
namespace NewMod.Buttons.WraithCaller
{
@@ -39,7 +39,7 @@ public class CallWraithButton : CustomActionButton
///
/// Default keybind for the Call Wraith ability.
///
- public override KeyboardKeyCode Defaultkeybind => KeyboardKeyCode.M;
+ public override MiraKeybind Keybind => MiraGlobalKeybinds.PrimaryAbility;
///
/// The duration of any effect triggered by this ability.
diff --git a/NewMod/Components/Birthday/Toast.cs b/NewMod/Components/Birthday/BirthdayToast.cs
similarity index 91%
rename from NewMod/Components/Birthday/Toast.cs
rename to NewMod/Components/Birthday/BirthdayToast.cs
index 7b3efe4..5aa66c6 100644
--- a/NewMod/Components/Birthday/Toast.cs
+++ b/NewMod/Components/Birthday/BirthdayToast.cs
@@ -9,7 +9,7 @@
using Il2CppInterop.Runtime.Attributes;
[RegisterInIl2Cpp]
-public class Toast(IntPtr ptr) : MonoBehaviour(ptr)
+public class BirthdayToast(IntPtr ptr) : MonoBehaviour(ptr)
{
public SpriteRenderer toastRend;
public TextMeshPro TimerText;
@@ -19,10 +19,10 @@ public void Awake()
toastRend = transform.Find("Background").GetComponent();
TimerText = transform.Find("Timer").GetComponent();
}
- public static Toast CreateToast()
+ public static BirthdayToast CreateBirthdayToast()
{
var gameObject = Instantiate(NewModAsset.Toast.LoadAsset(), HudManager.Instance.transform);
- var toast = gameObject.AddComponent();
+ var toast = gameObject.AddComponent();
return toast;
}
[HideFromIl2Cpp]
diff --git a/NewMod/Components/GeneralNPC.cs b/NewMod/Components/GeneralNPC.cs
new file mode 100644
index 0000000..f55065a
--- /dev/null
+++ b/NewMod/Components/GeneralNPC.cs
@@ -0,0 +1,142 @@
+/*using System;
+using System.Collections;
+using System.Linq;
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.GameOptions;
+using MiraAPI.Modifiers;
+using NewMod.Features;
+using NewMod.Options;
+using Reactor.Utilities;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components
+{
+ [RegisterInIl2Cpp]
+ public class GeneralNPC(IntPtr ptr) : MonoBehaviour(ptr)
+ {
+ public PlayerControl Owner { get; set; }
+ public PlayerControl npc;
+ public bool isActive = false;
+
+ [HideFromIl2Cpp]
+ public void Initialize(PlayerControl owner)
+ {
+ Owner = owner;
+
+ var prefab = AmongUsClient.Instance.PlayerPrefab;
+ npc = Instantiate(prefab);
+ npc.PlayerId = (byte)GameData.Instance.GetAvailableId();
+
+ var npcData = GameData.Instance.AddDummy(npc);
+ AmongUsClient.Instance.Spawn(npcData);
+ AmongUsClient.Instance.Spawn(npc);
+
+ npc.notRealPlayer = true;
+ KillAnimation.SetMovement(npc, true);
+ npc.NetTransform.RpcSnapTo(Owner.GetTruePosition());
+ npc.MyPhysics.Speed = OptionGroupSingleton.Instance.GeneralNPCSpeed.Value;
+
+ npc.RpcSetName("General NPC");
+ npc.RpcSetColor((byte)(npc.PlayerId % Palette.PlayerColors.Length));
+ npc.RpcSetHat("");
+ npc.RpcSetSkin("");
+ npc.RpcSetPet("");
+ npc.RpcSetVisor("");
+
+ if (!npc.TryGetComponent(out var mc))
+ {
+ mc = npc.gameObject.AddComponent();
+ }
+
+ npc.Collider.enabled = true;
+ npc.cosmetics.enabled = false;
+ npc.enabled = false;
+
+ isActive = true;
+ Coroutines.Start(WalkStopLoop());
+ }
+
+ [HideFromIl2Cpp]
+ public IEnumerator WalkStopLoop()
+ {
+ var runTime = OptionGroupSingleton.Instance.GeneralNPCRunTime.Value;
+ var stopTime = OptionGroupSingleton.Instance.GeneralNPCStopTime.Value;
+ while (isActive && !MeetingHud.Instance)
+ {
+ var pos = npc.GetTruePosition();
+ var current = RoomPathfinding.GetCurrentRoom(pos);
+ NewMod.Instance.Log.LogMessage($"[NPC] pos={pos} room={(current ? current.name : "none")}");
+
+ var target = RoomPathfinding.PickRandomOtherRoom(current);
+ if (!target)
+ {
+ NewMod.Instance.Log.LogWarning("[NPC] no target room, waiting");
+ npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
+ yield return new WaitForSeconds(stopTime);
+ continue;
+ }
+
+ var path = RoomPathfinding.FindRoomPath(current, target);
+ NewMod.Instance.Log.LogMessage($"[NPC] target={target.name} pathLen={path?.Count ?? 0}");
+
+ if (path == null || path.Count == 0)
+ {
+ npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
+ yield return new WaitForSeconds(stopTime);
+ continue;
+ }
+
+ foreach (var room in path)
+ {
+ if (!isActive || MeetingHud.Instance) break;
+
+ if (!RoomPathfinding.TryPickWaypointInside(room.roomArea, out var wp))
+ wp = (Vector2)room.roomArea.bounds.center;
+
+ NewMod.Instance.Log.LogMessage($"[NPC] moving to room={room.name} wp={wp}");
+
+ float timer = 0f;
+ while (isActive && !MeetingHud.Instance &&
+ Vector2.Distance(npc.GetTruePosition(), wp) > 0.05f)
+ {
+ var dir = (wp - npc.GetTruePosition()).normalized;
+ npc.MyPhysics.SetNormalizedVelocity(dir);
+
+ timer += Time.fixedDeltaTime;
+ if (timer >= runTime)
+ {
+ npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
+ NewMod.Instance.Log.LogMessage($"[NPC] burst stop {stopTime}s");
+ yield return new WaitForSeconds(stopTime);
+ timer = 0f;
+ }
+
+ yield return new WaitForFixedUpdate();
+ }
+
+ npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
+ NewMod.Instance.Log.LogMessage($"[NPC] arrived {room.name}, visiting {stopTime}s");
+ yield return new WaitForSeconds(stopTime);
+ }
+ }
+ npc.MyPhysics.SetNormalizedVelocity(Vector2.zero);
+ Dispose();
+ }
+ [HideFromIl2Cpp]
+ public void Dispose()
+ {
+ if (!isActive) return;
+ isActive = false;
+
+ if (npc != null)
+ {
+ var info = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(d => d.PlayerId == npc.PlayerId);
+ if (info != null) GameData.Instance.RemovePlayer(info.PlayerId);
+ Destroy(npc.gameObject);
+ npc = null;
+ }
+ Destroy(gameObject);
+ }
+ }
+}*/
diff --git a/NewMod/Components/ScreenEffects/EarthquackEffect.cs b/NewMod/Components/ScreenEffects/EarthquackEffect.cs
new file mode 100644
index 0000000..1138a8f
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/EarthquackEffect.cs
@@ -0,0 +1,35 @@
+using System;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects
+{
+ [RegisterInIl2Cpp]
+ public class EarthquakeEffect(IntPtr ptr) : MonoBehaviour(ptr)
+ {
+ public float amplitude = 2.5f;
+ public float frequency = 14f;
+ public float jitter = 0.6f;
+ public float ghost = 0.3f;
+ public float warp = 0.015f;
+ private readonly Shader _shader = NewModAsset.EarthquakeShader.LoadAsset();
+ private Material _mat;
+ public void OnEnable()
+ {
+ _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
+ }
+ public void OnDisable()
+ {
+ Destroy(_mat);
+ }
+ public void OnRenderImage(RenderTexture src, RenderTexture dst)
+ {
+ _mat.SetFloat("_Amplitude", amplitude);
+ _mat.SetFloat("_Frequency", frequency);
+ _mat.SetFloat("_Jitter", jitter);
+ _mat.SetFloat("_Ghost", ghost);
+ _mat.SetFloat("_Warp", warp);
+ Graphics.Blit(src, dst, _mat);
+ }
+ }
+}
diff --git a/NewMod/Components/ScreenEffects/GlitchEffect.cs b/NewMod/Components/ScreenEffects/GlitchEffect.cs
new file mode 100644
index 0000000..23db7e6
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/GlitchEffect.cs
@@ -0,0 +1,35 @@
+using System;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects
+{
+ [RegisterInIl2Cpp]
+ public class GlitchEffect(IntPtr ptr) : MonoBehaviour(ptr)
+ {
+ public float intensity = 0.45f;
+ public float blockSize = 64f;
+ public float colorSplit = 1.2f;
+ public float scanline = 0.15f;
+ public float speed = 4f;
+ private readonly Shader _shader = NewModAsset.GlitchShader.LoadAsset();
+ public Material _mat;
+ public void OnEnable()
+ {
+ _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
+ }
+ public void OnDisable()
+ {
+ Destroy(_mat);
+ }
+ public void OnRenderImage(RenderTexture src, RenderTexture dst)
+ {
+ _mat.SetFloat("_Intensity", intensity);
+ _mat.SetFloat("_BlockSize", blockSize);
+ _mat.SetFloat("_ColorSplit", colorSplit);
+ _mat.SetFloat("_Scanline", scanline);
+ _mat.SetFloat("_Speed", speed);
+ Graphics.Blit(src, dst, _mat);
+ }
+ }
+}
diff --git a/NewMod/Components/ScreenEffects/ShowPulseHueEffect.cs b/NewMod/Components/ScreenEffects/ShowPulseHueEffect.cs
new file mode 100644
index 0000000..33087cf
--- /dev/null
+++ b/NewMod/Components/ScreenEffects/ShowPulseHueEffect.cs
@@ -0,0 +1,32 @@
+using System;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+
+namespace NewMod.Components.ScreenEffects
+{
+ [RegisterInIl2Cpp]
+ public class SlowPulseHueEffect(IntPtr ptr) : MonoBehaviour(ptr)
+ {
+ public float hueSpeed = 0.35f;
+ public float saturation = 1.0f;
+ public float strength = 1.0f;
+ private readonly Shader _shader = NewModAsset.SlowPulseHueShader.LoadAsset();
+ private Material _mat;
+ public void OnEnable()
+ {
+ _mat = new Material(_shader) { hideFlags = HideFlags.DontSave };
+ }
+ public void OnDisable()
+ {
+ Destroy(_mat);
+ }
+
+ public void OnRenderImage(RenderTexture src, RenderTexture dst)
+ {
+ _mat.SetFloat("_HueSpeed", hueSpeed);
+ _mat.SetFloat("_Saturation", saturation);
+ _mat.SetFloat("_Strength", strength);
+ Graphics.Blit(src, dst, _mat);
+ }
+ }
+}
diff --git a/NewMod/Components/ShieldArea.cs b/NewMod/Components/ShieldArea.cs
new file mode 100644
index 0000000..00fb03e
--- /dev/null
+++ b/NewMod/Components/ShieldArea.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MiraAPI.GameOptions;
+using MiraAPI.Roles;
+using NewMod.Options.Roles.AegisOptions;
+using NewMod.Utilities;
+using Reactor.Utilities.Attributes;
+using UnityEngine;
+using static NewMod.Options.Roles.AegisOptions.AegisOptions;
+
+namespace NewMod.Components
+{
+ [RegisterInIl2Cpp]
+ public class ShieldArea(IntPtr ptr) : MonoBehaviour(ptr)
+ {
+ public byte ownerId;
+ public float radius;
+ public float duration;
+ float _t;
+ public static readonly List _active = new();
+ public static IEnumerable AreasAt(Vector2 pos)
+ => _active.Where(a => a && a.Contains(pos));
+
+ public static IEnumerable AreasOwnedBy(byte id)
+ => _active.Where(a => a && a.ownerId == id);
+
+ public static AegisMode Mode
+ => OptionGroupSingleton.Instance.Behavior;
+
+ public void Init(byte ownerId, float radius, float duration)
+ {
+ this.ownerId = ownerId;
+ this.radius = Mathf.Max(0.1f, radius);
+ this.duration = Mathf.Max(0.1f, duration);
+
+ var lp = PlayerControl.LocalPlayer;
+ bool shouldSee = false;
+
+ if (lp.PlayerId == ownerId)
+ {
+ shouldSee = OptionGroupSingleton.Instance.Visibility != WardVisibilityMode.AllPlayers
+ || true;
+ }
+ if (!shouldSee)
+ {
+ switch (OptionGroupSingleton.Instance.Visibility)
+ {
+ case WardVisibilityMode.AllPlayers:
+ shouldSee = true;
+ break;
+ case WardVisibilityMode.TeamOnly:
+ var role = lp.Data.Role;
+ var isCrew = role && role.TeamType == RoleTeamTypes.Crewmate;
+
+ if (isCrew && CustomRoleManager.GetCustomRoleBehaviour(role.Role, out var customRole) && customRole != null)
+ {
+ isCrew = customRole.Team == ModdedRoleTeams.Crewmate;
+ }
+ shouldSee = isCrew;
+ break;
+
+ case WardVisibilityMode.OwnerOnly:
+ break;
+ }
+ }
+ if (shouldSee)
+ {
+ Utils.CreateCircle(
+ "AegisShieldVisual",
+ (Vector2)transform.position,
+ this.radius,
+ new Color(0.227f, 0.651f, 1f, 0.35f),
+ this.duration
+ );
+ }
+ }
+
+ public void Awake()
+ {
+ if (!_active.Contains(this)) _active.Add(this);
+ }
+
+ public void OnDestroy()
+ {
+ _active.Remove(this);
+ }
+
+ public void Update()
+ {
+ _t += Time.deltaTime;
+ if (_t >= duration)
+ {
+ Destroy(gameObject);
+ }
+ }
+
+ public bool Contains(Vector2 worldPos)
+ {
+ var center = (Vector2)transform.position;
+ return Vector2.Distance(worldPos, center) <= radius;
+ }
+ public static bool IsInsideAny(Vector2 pos) => _active.Any(a => a && a.Contains(pos));
+
+ public static bool IsInsideOthersWard(PlayerControl player)
+ {
+ if (!player) return false;
+ var pos = player.GetTruePosition();
+ return _active.Any(a => a && a.ownerId != player.PlayerId && a.Contains(pos));
+ }
+ public static bool IsInsideOthersWardAt(Vector2 pos, byte sourceId)
+ {
+ return _active.Any(a => a && a.ownerId != sourceId && a.Contains(pos));
+ }
+ }
+}
diff --git a/NewMod/Components/SlashTray.cs b/NewMod/Components/SlashTray.cs
new file mode 100644
index 0000000..808faba
--- /dev/null
+++ b/NewMod/Components/SlashTray.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections;
+using Reactor.Utilities;
+using TMPro;
+using UnityEngine;
+using NewMod;
+using NewMod.Utilities;
+using Reactor.Utilities.Attributes;
+using MiraAPI.Networking;
+using MiraAPI.GameOptions;
+using NewMod.Options.Roles.EdgeveilOptions;
+
+[RegisterInIl2Cpp]
+public class SlashTray(IntPtr ptr) : MonoBehaviour(ptr)
+{
+ public PlayerControl Owner { get; set; }
+ public SpriteRenderer SlashTrayRend;
+ public Vector2 _dir;
+ public float _speed;
+ public int _kills;
+ public void Awake()
+ {
+ SlashTrayRend = transform.Find("Background").GetComponent();
+ var rb = transform.gameObject.AddComponent();
+ rb.isKinematic = true;
+ rb.simulated = true;
+ }
+ public static SlashTray CreateTray()
+ {
+ var gameObject = Instantiate(NewModAsset.SlashTray.LoadAsset(), HudManager.Instance.transform);
+ var tray = gameObject.AddComponent();
+ return tray;
+ }
+ public void Update()
+ {
+ transform.position += (Vector3)(_dir * _speed * Time.deltaTime);
+ }
+
+ public void SetMotion(Vector2 dir, float speed)
+ {
+ _dir = dir.normalized;
+ _speed = speed;
+ }
+
+ public void OnTriggerEnter2D(Collider2D other)
+ {
+ NewMod.NewMod.Instance.Log.LogMessage($"Hit {other.name}");
+ var pc = other.GetComponentInParent();
+ if (pc == null) return;
+ if (pc == Owner) return;
+ if (pc.Data.IsDead) return;
+
+ PlayerControl.LocalPlayer.RpcCustomMurder(pc, teleportMurderer: false);
+
+ _kills++;
+ if (_kills >= (int)OptionGroupSingleton.Instance.PlayersToKill)
+ {
+ Destroy(gameObject);
+ }
+ }
+}
\ No newline at end of file
diff --git a/NewMod/Components/Toast.cs b/NewMod/Components/Toast.cs
new file mode 100644
index 0000000..e677f36
--- /dev/null
+++ b/NewMod/Components/Toast.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections;
+using Reactor.Utilities;
+using TMPro;
+using UnityEngine;
+using Reactor.Utilities.Attributes;
+using NewMod;
+
+[RegisterInIl2Cpp]
+public class Toast(IntPtr ptr) : MonoBehaviour(ptr)
+{
+ public SpriteRenderer toastRend;
+ public SpriteRenderer nmLogo;
+ public TextMeshPro TitleText;
+ public TextMeshPro Text;
+ public void Awake()
+ {
+ toastRend = transform.Find("Background").GetComponent();
+ nmLogo = transform.FindChild("Background/NM").GetComponent();
+ TitleText = transform.FindChild("Background/TitleText").GetComponent();
+ Text = transform.Find("Text").GetComponent();
+
+ transform.localScale = new Vector3(0.3f, 0.1636f, 1f);
+ nmLogo.sortingOrder = 100;
+ }
+ public static Toast CreateToast()
+ {
+ var gameObject = Instantiate(NewModAsset.Toast.LoadAsset(), HudManager.Instance.transform);
+ var toast = gameObject.AddComponent();
+ return toast;
+ }
+ public void ShowToast(string title, string text, Color color, float displayDuration)
+ {
+ if (!LobbyBehaviour.Instance) return;
+
+ TitleText.text = title;
+ Text.text = text;
+ Text.color = color;
+
+ Coroutines.Start(CoAnimateToast(displayDuration));
+ }
+ public IEnumerator CoAnimateToast(float duration)
+ {
+ Vector3 visiblePos = new Vector3(-0.0527f, 2.7741f, 0f);
+ Vector3 hiddenPos = visiblePos + new Vector3(0f, 1.5f, 0f);
+
+ transform.localPosition = hiddenPos;
+
+ float t = 0f;
+ const float slideTime = 0.4f;
+ while (t < 1f)
+ {
+ t += Time.deltaTime / slideTime;
+ transform.localPosition = Vector3.Lerp(hiddenPos, visiblePos, Mathf.SmoothStep(0, 1, t));
+ yield return null;
+ }
+
+ yield return new WaitForSeconds(duration);
+
+ t = 0f;
+ while (t < 1f)
+ {
+ t += Time.deltaTime / slideTime;
+ transform.localPosition = Vector3.Lerp(visiblePos, hiddenPos, Mathf.SmoothStep(0, 1, t));
+ yield return null;
+ }
+ Destroy(gameObject);
+ }
+}
diff --git a/NewMod/Components/WraithCallerNpc.cs b/NewMod/Components/WraithCallerNpc.cs
index b1d834f..599081d 100644
--- a/NewMod/Components/WraithCallerNpc.cs
+++ b/NewMod/Components/WraithCallerNpc.cs
@@ -1,7 +1,10 @@
using System;
using System.Linq;
using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.GameOptions;
+using MiraAPI.Modifiers;
using MiraAPI.Networking;
+using NewMod.Options.Roles.WraithCallerOptions;
using NewMod.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Attributes;
@@ -38,6 +41,7 @@ public void Initialize(PlayerControl wraith, PlayerControl target)
npc.notRealPlayer = true;
KillAnimation.SetMovement(npc, true);
npc.NetTransform.RpcSnapTo(Owner.transform.position);
+ npc.MyPhysics.Speed = OptionGroupSingleton.Instance.NPCSpeed;
var color = (byte)(npc.PlayerId % Palette.PlayerColors.Length);
npc.RpcSetName("Wraith NPC");
@@ -56,10 +60,18 @@ public void Initialize(PlayerControl wraith, PlayerControl target)
noShadow.hitOverride = npc.Collider;
}
- ownerLight = Owner.lightSource;
- ownerLight.transform.SetParent(npc.transform, false);
- ownerLight.transform.localPosition = npc.Collider.offset;
- Camera.main.GetComponent().SetTarget(npc);
+ if (!npc.TryGetComponent(out var mc))
+ {
+ mc = npc.gameObject.AddComponent();
+ }
+
+ if (OptionGroupSingleton.Instance.ShouldSwitchCamToNPC)
+ {
+ Camera.main.GetComponent().SetTarget(npc);
+ ownerLight = Owner.lightSource;
+ ownerLight.transform.SetParent(npc.transform, false);
+ ownerLight.transform.localPosition = npc.Collider.offset;
+ }
npc.cosmetics.enabled = false;
npc.enabled = false;
@@ -73,11 +85,6 @@ public void Initialize(PlayerControl wraith, PlayerControl target)
SoundManager.Instance.PlaySound(NewModAsset.HeartbeatSound.LoadAsset(), false, 1f);
}
}
- public void Update()
- {
- if (MeetingHud.Instance)
- Dispose();
- }
[HideFromIl2Cpp]
public System.Collections.IEnumerator WalkToTarget()
{
@@ -120,18 +127,21 @@ public System.Collections.IEnumerator WalkToTarget()
public void Dispose()
{
if (!isActive) return;
+
isActive = false;
if (npc != null)
{
- var cam = Camera.main.GetComponent();
- cam.SetTarget(Owner);
-
- ownerLight.transform.SetParent(Owner.transform, false);
- ownerLight.transform.localPosition = Owner.Collider.offset;
-
+ if (OptionGroupSingleton.Instance.ShouldSwitchCamToNPC)
+ {
+ var cam = Camera.main.GetComponent();
+ cam.SetTarget(Owner);
+ ownerLight.transform.SetParent(Owner.transform, false);
+ ownerLight.transform.localPosition = Owner.Collider.offset;
+ }
var info = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(d => d.PlayerId == npc.PlayerId);
- if (info != null) GameData.Instance.RemovePlayer(info.PlayerId);
+ GameData.Instance.RemovePlayer(info.PlayerId);
+ PlayerControl.AllPlayerControls.Remove(npc);
Destroy(npc.gameObject);
npc = null;
diff --git a/NewMod/DebugWindow.cs b/NewMod/DebugWindow.cs
index 6f2bc4f..2e191a4 100644
--- a/NewMod/DebugWindow.cs
+++ b/NewMod/DebugWindow.cs
@@ -1,4 +1,4 @@
-using AmongUs.GameOptions;
+using UnityEngine;
using System.Linq;
using System;
using MiraAPI.Hud;
@@ -11,119 +11,131 @@
using NewMod.Roles.NeutralRoles;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.ImGui;
-using UnityEngine;
-using UnityEngine.Events;
using Il2CppInterop.Runtime.Attributes;
-using NewMod.Buttons.Overload;
+using AmongUs.GameOptions;
+using NewMod.Components.ScreenEffects;
+using Reactor.Utilities;
namespace NewMod
{
[RegisterInIl2Cpp]
public class DebugWindow(nint ptr) : MonoBehaviour(ptr)
{
- [HideFromIl2Cpp]
- public bool EnableDebugger { get; set; } = false;
+ [HideFromIl2Cpp] public bool EnableDebugger { get; set; } = false;
+ public float Zoom = 3f;
+ public const float ZoomMin = 3f;
+ public const float ZoomMax = 15f;
+ public bool ScrollZoomWhileOpen = true;
+ public static DebugWindow Instance;
+
+ public void ApplyZoom(float size)
+ {
+ size = Mathf.Clamp(size, ZoomMin, ZoomMax);
+ if (Camera.main) Camera.main.orthographicSize = size;
+ foreach (var cam in Camera.allCameras) if (cam) cam.orthographicSize = size;
+ ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen);
+ if (HudManager.Instance && HudManager.Instance.ShadowQuad)
+ {
+ bool zoomingOut = size > 3f;
+ HudManager.Instance.ShadowQuad.gameObject.SetActive(!zoomingOut);
+ }
+ }
+
+ private static bool AllowDebug()
+ {
+ return AmongUsClient.Instance && AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay;
+ }
+
public readonly DragWindow DebuggingWindow = new(new Rect(10, 10, 0, 0), "NewMod Debug Window", () =>
{
- bool isFreeplay = AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay;
+ bool allow = AllowDebug();
- if (GUILayout.Button("Become Explosive Modifier"))
+ GUILayout.BeginVertical(GUI.skin.box);
+ GUILayout.Label("Camera Zoom");
+ var newZoom = GUILayout.HorizontalSlider(Instance.Zoom, ZoomMin, ZoomMax, GUILayout.Width(220f));
+ GUILayout.BeginHorizontal();
+ if (GUILayout.Button("-", GUILayout.Width(28f)) && allow) newZoom = Mathf.Clamp(Instance.Zoom / 1.25f, ZoomMin, ZoomMax);
+ if (GUILayout.Button("+", GUILayout.Width(28f)) && allow) newZoom = Mathf.Clamp(Instance.Zoom * 1.25f, ZoomMin, ZoomMax);
+ if (GUILayout.Button("Reset", GUILayout.Width(64f)) && allow) newZoom = 3f;
+ Instance.ScrollZoomWhileOpen = GUILayout.Toggle(Instance.ScrollZoomWhileOpen, "Scroll-wheel zoom");
+ GUILayout.EndHorizontal();
+ if (!Mathf.Approximately(newZoom, Instance.Zoom) && allow)
{
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcAddModifier();
+ Instance.Zoom = newZoom;
+ Instance.ApplyZoom(Instance.Zoom);
}
- if (GUILayout.Button("Remove Explosive Modifier"))
+ GUILayout.Label($"Size: {Instance.Zoom:0.00}");
+ GUILayout.EndVertical();
+
+ GUILayout.Space(6);
+
+ if (GUILayout.Button("Become Explosive Modifier") && allow) PlayerControl.LocalPlayer.RpcAddModifier();
+ if (GUILayout.Button("Remove Explosive Modifier") && allow) PlayerControl.LocalPlayer.RpcRemoveModifier();
+ if (GUILayout.Button("Become Necromancer") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ if (GUILayout.Button("Become DoubleAgent") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ if (GUILayout.Button("Become EnergyThief") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ if (GUILayout.Button("Become SpecialAgent") && allow) PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ if (GUILayout.Button("Force Start Game") && allow) if (GameOptionsManager.Instance.CurrentGameOptions.NumImpostors is not 1) AmongUsClient.Instance.StartGame();
+ if (GUILayout.Button("Increases Uses by 3") && allow) foreach (var button in CustomButtonManager.Buttons) button.SetUses(3);
+ if (GUILayout.Button("Randomly Cast a Vote") && allow && MeetingHud.Instance)
{
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcRemoveModifier();
+ var randPlayer = Utils.GetRandomPlayer(p => !p.Data.IsDead && !p.Data.Disconnected);
+ MeetingHud.Instance.CmdCastVote(PlayerControl.LocalPlayer.PlayerId, randPlayer.PlayerId);
}
- if (GUILayout.Button("Become Necromancer"))
+ if (GUILayout.Button("End Meeting") && allow && MeetingHud.Instance)
{
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ MeetingHud.Instance.Close();
}
- if (GUILayout.Button("Become DoubleAgent"))
+ if (GUILayout.Button("Apply Glitch Effect to Main Camera") && allow)
{
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ Camera.main.gameObject.AddComponent();
}
- if (GUILayout.Button("Become EnergyThief"))
+ if (GUILayout.Button("Apply Earthquake Effect to Main Camera") && allow)
{
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ Camera.main.gameObject.AddComponent();
}
- if (GUILayout.Button("Become SpecialAgent"))
+ if (GUILayout.Button("Apply Slow Hue Pulse Effect to Main Camera") && allow)
{
- if (!isFreeplay) return;
- PlayerControl.LocalPlayer.RpcSetRole((RoleTypes)RoleId.Get(), false);
+ Camera.main.gameObject.AddComponent();
}
- if (GUILayout.Button("Force Start Game"))
+ if (GUILayout.Button("Reset Camera Effects") && allow)
{
- if (GameOptionsManager.Instance.CurrentGameOptions.NumImpostors is 1) return;
- AmongUsClient.Instance.StartGame();
+ Coroutines.Start(CoroutinesHelper.RemoveCameraEffect(Camera.main, 1f));
}
- if (GUILayout.Button("Increases Uses by 3"))
+ if (GUILayout.Button("Show Toast") && LobbyBehaviour.Instance)
{
- foreach (var button in CustomButtonManager.Buttons)
- {
- button.SetUses(3);
- }
+ Toast.CreateToast().ShowToast(string.Empty, "NewMod v1.2.6", Color.red, 5f);
}
- if (GUILayout.Button("Randomly Cast a Vote"))
+ /*if (GUILayout.Button("Spawn General NPC") && allow)
{
- if (!MeetingHud.Instance) return;
- var randPlayer = Utils.GetRandomPlayer(p => !p.Data.IsDead && !p.Data.Disconnected);
- MeetingHud.Instance.CmdCastVote(PlayerControl.LocalPlayer.PlayerId, randPlayer.PlayerId);
- }
- GUILayout.Space(4);
-
- GUILayout.Label("Overload button tests", GUI.skin.box);
-
- if (GUILayout.Button("Test Absorb"))
- {
- 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 && Utils.RoleToButtonsMap.TryGetValue(customRole.GetType(), out var buttonsType))
- {
- Debug.Log("Starting to absorb ability...");
-
- foreach (var buttonType in buttonsType)
- {
- var button = CustomButtonManager.Buttons.FirstOrDefault(b => b.GetType() == buttonType);
-
- if (button != null)
- {
- CustomButtonSingleton.Instance.Absorb(button);
- }
- Debug.Log($"[Overload] Successfully absorbed ability: {button.Name}, CanUse:{button.CanUse()}");
- }
- }
- 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);
- }
- }
- }
+ var npc = new GameObject("GeneralNPC").AddComponent();
+ npc.Initialize(PlayerControl.LocalPlayer);
+ }*/
});
+
public void OnGUI()
{
if (EnableDebugger) DebuggingWindow.OnGUI();
}
+
+ public void Start()
+ {
+ Instance = this;
+ if (Camera.main) Zoom = Mathf.Clamp(Camera.main.orthographicSize, ZoomMin, ZoomMax);
+ }
+
public void Update()
{
- if (Input.GetKey(KeyCode.F3))
+ if (Input.GetKeyDown(KeyCode.F3)) EnableDebugger = !EnableDebugger;
+ if (EnableDebugger && ScrollZoomWhileOpen && AllowDebug())
{
- EnableDebugger = !EnableDebugger;
+ float wheel = Input.GetAxis("Mouse ScrollWheel");
+ if (Mathf.Abs(wheel) > 0.0001f)
+ {
+ var factor = wheel > 0 ? 1f / 1.25f : 1.25f;
+ Zoom = Mathf.Clamp(Zoom * factor, ZoomMin, ZoomMax);
+ ApplyZoom(Zoom);
+ }
}
}
}
diff --git a/NewMod/DiscordStatus.cs b/NewMod/DiscordStatus.cs
index 672a735..c392733 100644
--- a/NewMod/DiscordStatus.cs
+++ b/NewMod/DiscordStatus.cs
@@ -46,15 +46,14 @@ public static void UpdateActivityPrefix([HarmonyArgument(0)] ref Activity activi
{
if (activity == null) return;
- var isBeta = true;
+ var isBeta = false;
string details = $"NewMod v{NewMod.ModVersion}" + (isBeta ? " (Beta)" : " (Dev)");
activity.Details = details;
activity.State = $"Playing Among Us | NewMod v{NewMod.ModVersion}";
activity.Assets = new ActivityAssets()
{
- LargeImage = "newmodlogov1_2_0",
- SmallImage = "nm",
+ LargeImage = "nm",
SmallText = "Made with MiraAPI"
};
diff --git a/NewMod/Extensions/RoleExtensions.cs b/NewMod/Extensions/RoleExtensions.cs
deleted file mode 100644
index 698cc9d..0000000
--- a/NewMod/Extensions/RoleExtensions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using MiraAPI.Roles;
-using NewMod.Roles;
-
-namespace NewMod.Extensions
-{
- public static class RoleExtensions
- {
- public static bool IsNewModRoleFaction(this ICustomRole role) => role is INewModRole;
- }
-}
\ No newline at end of file
diff --git a/NewMod/Features/CustomPlayerTag.cs b/NewMod/Features/CustomPlayerTag.cs
deleted file mode 100644
index b06306e..0000000
--- a/NewMod/Features/CustomPlayerTag.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-using System;
-using System.Collections.Generic;
-using HarmonyLib;
-using Hazel;
-using MiraAPI.Events;
-using MiraAPI.Events.Vanilla.Meeting;
-using Reactor.Utilities;
-
-namespace NewMod.Features
-{
- public static class CustomPlayerTag
- {
- public enum TagType : byte
- {
- Player,
- NPC,
- Dev,
- Creator,
- Tester,
- Staff,
- Contributor,
- Host,
- AOUDev
- }
-
- public static readonly Dictionary DefaultHex = new()
- {
- { TagType.Player, "c0c0c0" },
- { TagType.NPC, "7D3C98"},
- { TagType.Dev, "ff4d4d" },
- { TagType.Creator, "ffb000" },
- { TagType.Tester, "00e0ff" },
- { TagType.Staff, "9b59b6" },
- { TagType.Contributor, "7ee081" },
- { TagType.Host, "ff7f50" },
- { TagType.AOUDev, "00ffb3" }
- };
-
- public static string DisplayName(TagType t) => t switch
- {
- TagType.Player => "Player",
- TagType.NPC => "NPC",
- TagType.Dev => "Developer",
- TagType.Creator => "Creator",
- TagType.Tester => "Tester",
- TagType.Staff => "Staff",
- TagType.Contributor => "Contributor",
- TagType.Host => "Host",
- TagType.AOUDev => "AOU Dev",
- _ => ""
- };
-
- public static string Format(TagType t, string hex)
- {
- string color = string.IsNullOrWhiteSpace(hex)
- ? (DefaultHex.TryGetValue(t, out var h) ? h : "ffffff")
- : hex;
- string label = DisplayName(t);
- return $"\n{label}";
- }
- public static TagType GetTag(string friendCode)
- {
- if (string.Equals(friendCode, "puncool#9009", StringComparison.OrdinalIgnoreCase)) return TagType.Creator;
- if (string.Equals(friendCode, "peaktipple#8186", StringComparison.OrdinalIgnoreCase)) return TagType.Dev;
- if (string.Equals(friendCode, "shinyrake#9382", StringComparison.OrdinalIgnoreCase)) return TagType.Dev;
- if (string.Equals(friendCode, "dimpledue#6629", StringComparison.OrdinalIgnoreCase)) return TagType.AOUDev;
- return TagType.Player;
- }
- public static TagType GetTagFor(PlayerControl pc)
- {
- if (pc.isDummy || pc.notRealPlayer) return TagType.NPC; // Will affect dummies in Freeplay mode
- return GetTag(pc.FriendCode);
- }
-
- [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSetName))]
- public static class RpcSetNamePatch
- {
- public static bool Prefix(PlayerControl __instance, ref string name)
- {
- var friendCode = __instance.FriendCode;
-
- TagType tag = GetTagFor(__instance);
-
- var host = GameData.Instance.GetHost();
- bool isHost = host.PlayerId == __instance.PlayerId;
-
- string baseName = name.Split('\n')[0];
-
- string newName = baseName;
- if (isHost)
- newName += Format(TagType.Host, DefaultHex[TagType.Host]);
- if (tag != TagType.Player)
- newName += Format(tag, DefaultHex[tag]);
- else
- newName += Format(TagType.Player, DefaultHex[TagType.Player]);
-
- Logger.Instance.LogInfo($"Player {__instance.PlayerId} '{baseName}' " + $"FriendCode={friendCode}, Host={isHost}, Tag={DisplayName(tag)}");
-
- __instance.SetName(newName);
-
- var writer = AmongUsClient.Instance.StartRpcImmediately(__instance.NetId, (byte)RpcCalls.SetName, SendOption.Reliable, -1);
- writer.Write(__instance.Data.NetId);
- writer.Write(newName);
- AmongUsClient.Instance.FinishRpcImmediately(writer);
-
- return false;
- }
- }
-
- [RegisterEvent]
- public static void OnMeetingStart(StartMeetingEvent evt)
- {
- var host = GameData.Instance.GetHost();
-
- foreach (var ps in evt.MeetingHud.playerStates)
- {
- string baseName = ps.NameText.text.Split('\n')[0];
- bool isHost = ps.TargetPlayerId == host.PlayerId;
-
- TagType tag = GetTag(GameData.Instance.GetPlayerById(ps.TargetPlayerId).FriendCode);
- string newName = baseName;
-
- if (isHost)
- newName += Format(TagType.Host, DefaultHex[TagType.Host]);
-
- if (tag != TagType.Player)
- newName += Format(tag, DefaultHex[tag]);
- else
- newName += Format(TagType.Player, DefaultHex[TagType.Player]);
-
- ps.NameText.text = newName;
- }
- }
- }
-}
diff --git a/NewMod/NewMod.cs b/NewMod/NewMod.cs
index 9578725..911fb68 100644
--- a/NewMod/NewMod.cs
+++ b/NewMod/NewMod.cs
@@ -36,7 +36,7 @@ namespace NewMod;
public partial class NewMod : BasePlugin, IMiraPlugin
{
public const string Id = "com.callofcreator.newmod";
- public const string ModVersion = "1.2.5";
+ public const string ModVersion = "1.2.6";
public Harmony Harmony { get; } = new Harmony(Id);
public static BasePlugin Instance;
public static Minigame minigame;
@@ -80,19 +80,17 @@ public static void Postfix(KeyboardJoystick __instance)
}
public static void InitializeKeyBinds()
{
- if (AmongUsClient.Instance.GameState != InnerNet.InnerNetClient.GameStates.Started) return;
-
- if (Input.GetKeyDown(KeyCode.F2) && PlayerControl.LocalPlayer.Data.Role.Role is AmongUs.GameOptions.RoleTypes.Crewmate && OptionGroupSingleton.Instance.CanOpenCams)
+ if (Input.GetKeyDown(KeyCode.F2) && PlayerControl.LocalPlayer.Data.IsDead && OptionGroupSingleton.Instance.AllowCams)
{
- var cam = Object.FindObjectsOfType().FirstOrDefault(x => x.name.Contains("Surv"));
- if (Camera.main is not null || cam != null)
- {
- minigame = Object.Instantiate(cam.MinigamePrefab, Camera.main.transform, false);
- minigame.transform.localPosition = new Vector3(0f, 0f, -50f);
- minigame.Begin(null);
- }
+ var sys = Utils.FindSurveillanceConsole();
+ var mainCam = Camera.main;
+ if (mainCam == null) return;
+
+ var minigame = Object.Instantiate(sys.MinigamePrefab, mainCam.transform, false);
+ minigame.transform.localPosition = new Vector3(0f, 0f, -50f);
+ minigame.Begin(null);
}
- if (Input.GetKeyDown(KeyCode.F3) && PlayerControl.LocalPlayer.Data.Role is NecromancerRole && OptionGroupSingleton.Instance.EnableTeleportation)
+ if (Input.GetKeyDown(KeyCode.F3) && PlayerControl.LocalPlayer.Data.Role is NecromancerRole)
{
var deadBodies = Helpers.GetNearestDeadBodies(PlayerControl.LocalPlayer.GetTruePosition(), 20f, Helpers.CreateFilter(Constants.NotShipMask));
if (deadBodies != null && deadBodies.Count > 0)
@@ -170,9 +168,9 @@ public static class SetTaskTextPatch
{
public static void Postfix(TaskPanelBehaviour __instance, [HarmonyArgument(0)] string str)
{
- if (AmongUsClient.Instance.GameState == InnerNet.InnerNetClient.GameStates.Started && PlayerControl.LocalPlayer.Data.Role.Role is AmongUs.GameOptions.RoleTypes.Crewmate)
+ if (PlayerControl.LocalPlayer.Data.IsDead)
{
- __instance.taskText.text += "\n" + (OptionGroupSingleton.Instance.CanOpenCams ? "Press F2 For Open Cams" : "You cannot open cams because the host has disabled this setting");
+ __instance.taskText.text += "\n" + (OptionGroupSingleton.Instance.AllowCams ? "Press F2 For Open Cams" : "You cannot open cams because the host has disabled this setting");
}
}
}
diff --git a/NewMod/NewMod.csproj b/NewMod/NewMod.csproj
index 7025540..5d8245a 100644
--- a/NewMod/NewMod.csproj
+++ b/NewMod/NewMod.csproj
@@ -1,6 +1,6 @@
- 1.2.5
+ 1.2.6
dev
NewMod is a mod for Among Us that introduces a variety of new roles, unique abilities
CallofCreator
@@ -21,7 +21,6 @@
-
@@ -29,7 +28,13 @@
+
+
+ ..\libs\MiraAPI.dll
+
+
+
-
+
\ No newline at end of file
diff --git a/NewMod/NewModAsset.cs b/NewMod/NewModAsset.cs
index 6d66272..33cef04 100644
--- a/NewMod/NewModAsset.cs
+++ b/NewMod/NewModAsset.cs
@@ -6,20 +6,27 @@ namespace NewMod;
public static class NewModAsset
{
+#pragma warning disable CA2211
public static AssetBundle Bundle = AssetBundleManager.Load("newmod");
+#pragma warning restore CA2211
// Miscellaneous
public static LoadableResourceAsset Banner { get; } = new("NewMod.Resources.optionImage.png");
public static LoadableResourceAsset Arrow { get; } = new("NewMod.Resources.Arrow.png");
public static LoadableResourceAsset ModLogo { get; } = new("NewMod.Resources.Logo.png");
public static LoadableResourceAsset CustomCursor { get; } = new("NewMod.Resources.cursor.png");
+ public static LoadableAsset Toast { get; } = new LoadableBundleAsset("Toast", Bundle);
+ public static LoadableAsset SlashTray { get; } = new LoadableBundleAsset("SlashTray", Bundle);
// NewMod's First Birthday Assets
public static LoadableResourceAsset MainMenuBG { get; } = new("NewMod.Resources.Birthday.newmod-birthday-v1.png");
public static LoadableAsset CustomLobby { get; } = new LoadableBundleAsset("CustomLobby", Bundle);
- public static LoadableAsset Toast { get; } = new LoadableBundleAsset("Toast", Bundle);
+ public static LoadableAsset BirthdayToast { get; } = new LoadableBundleAsset("BirthdayToast", Bundle);
public static LoadableAsset WraithCallerMinigame { get; } = new LoadableBundleAsset("WraithCallerMinigame", Bundle);
+ // NewMod First Halloween Assets
+ public static LoadableAsset HalloweenLobby { get; } = new LoadableBundleAsset("HalloweenLobby", Bundle);
+
// Button icons
public static LoadableResourceAsset SpecialAgentButton { get; } = new("NewMod.Resources.givemission.png");
public static LoadableResourceAsset ShowScreenshotButton { get; } = new("NewMod.Resources.showscreenshot.png");
@@ -31,6 +38,8 @@ public static class NewModAsset
public static LoadableResourceAsset StrikeButton { get; } = new("NewMod.Resources.Strike.png");
public static LoadableResourceAsset FinalButton { get; } = new("NewMod.Resources.final.png");
public static LoadableResourceAsset CallWraith { get; } = new("NewMod.Resources.callwraith.png");
+ public static LoadableResourceAsset Shield { get; } = new("NewMod.Resources.Shield.png");
+ public static LoadableResourceAsset Slash { get; } = new("NewMod.Resources.Slash.png");
// SFX
public static LoadableAudioResourceAsset ReviveSound { get; } = new("NewMod.Resources.Sounds.revive.wav");
@@ -46,11 +55,19 @@ public static class NewModAsset
// Role Icons
public static LoadableResourceAsset StrikeIcon { get; } = new("NewMod.Resources.RoleIcons.StrikeIcon.png");
public static LoadableResourceAsset InjectIcon { get; } = new("NewMod.Resources.RoleIcons.InjectIcon.png");
- public static LoadableResourceAsset CrownIcon { get; } = new("NewMod.Resources.RoleIcons.crown.png");
- public static LoadableResourceAsset WraithIcon { get; } = new("NewMod.Resources.RoleIcons.wraith.png");
+ public static LoadableResourceAsset CrownIcon { get; } = new("NewMod.Resources.RoleIcons.CrownIcon.png");
+ public static LoadableResourceAsset WraithIcon { get; } = new("NewMod.Resources.RoleIcons.WraithIcon.png");
+ public static LoadableResourceAsset ShieldIcon { get; } = new("NewMod.Resources.RoleIcons.ShieldIcon.png");
+ public static LoadableResourceAsset RadarIcon { get; } = new("NewMod.Resources.RoleIcons.RadarIcon.png");
+ public static LoadableResourceAsset SlashIcon { get; } = new("NewMod.Resources.RoleIcons.SlashIcon.png");
// Notif Icons
public static LoadableResourceAsset VisionDebuff { get; } = new("NewMod.Resources.NotifIcons.vision_debuff.png");
public static LoadableResourceAsset SpeedDebuff { get; } = new("NewMod.Resources.NotifIcons.speed_debuff.png");
public static LoadableResourceAsset Freeze { get; } = new("NewMod.Resources.NotifIcons.freeze.png");
+
+ // Shaders
+ public static LoadableAsset GlitchShader { get; } = new LoadableBundleAsset("GlitchFullScreen.shader", Bundle);
+ public static LoadableAsset EarthquakeShader { get; } = new LoadableBundleAsset("EarthquakeFullScreen.shader", Bundle);
+ public static LoadableAsset SlowPulseHueShader { get; } = new LoadableBundleAsset("SlowPulseHue.shader", Bundle);
}
\ No newline at end of file
diff --git a/NewMod/NewModDateTime.cs b/NewMod/NewModDateTime.cs
index d1d1b4d..049d833 100644
--- a/NewMod/NewModDateTime.cs
+++ b/NewMod/NewModDateTime.cs
@@ -4,24 +4,82 @@ namespace NewMod
{
public static class NewModDateTime
{
- public static DateTime NewModBirthday
+ private const int Month = 8;
+ private const int Day = 28;
+ private const int Hour = 16;
+ public static readonly TimeSpan Window = TimeSpan.FromDays(8);
+ public static DateTime BirthdayStartThisYear =>
+ new(DateTime.Now.Year, Month, Day, Hour, 0, 0, DateTimeKind.Local);
+
+ public static DateTime BirthdayStartNextYear =>
+ new(DateTime.Now.Year + 1, Month, Day, Hour, 0, 0, DateTimeKind.Local);
+
+ public static DateTime UpcomingBirthdayStart
{
get
{
- var thisYear = new DateTime(DateTime.Now.Year, 8, 28, 16, 0, 0);
- return DateTime.Now <= thisYear ? thisYear : new DateTime(DateTime.Now.Year + 1, 8, 28);
+ var now = DateTime.Now;
+ var startThis = BirthdayStartThisYear;
+ var endThis = startThis + Window;
+
+ if (now < startThis) return startThis;
+ if (now < endThis) return startThis;
+
+ return BirthdayStartNextYear;
}
}
- public static DateTime NewModBirthdayWeekEnd
+ public static DateTime BirthdayWindowEndThisYear => BirthdayStartThisYear + Window;
+
+ public static bool IsNewModBirthdayWeek
{
get
{
- return NewModBirthday.AddDays(7);
+ var now = DateTime.Now;
+ var start = BirthdayStartThisYear;
+ var end = start + Window;
+ return now >= start && now < end;
}
}
+ public static bool IsWraithCallerUnlocked => DateTime.Now >= BirthdayStartThisYear;
+ private const int HalloweenMonth = 10;
+ private const int HalloweenDay = 31;
+ public static readonly TimeSpan HalloweenWindow = TimeSpan.FromDays(7);
+
+ public static DateTime HalloweenStartThisYear =>
+ new DateTime(DateTime.Now.Year, HalloweenMonth, HalloweenDay, 0, 0, 0, DateTimeKind.Local);
- public static bool IsNewModBirthdayWeek =>
- DateTime.Now >= NewModBirthday && DateTime.Now <= NewModBirthdayWeekEnd;
- public static bool IsWraithCallerUnlocked => DateTime.Now >= NewModBirthday;
+ public static DateTime HalloweenStartNextYear =>
+ new DateTime(DateTime.Now.Year + 1, HalloweenMonth, HalloweenDay, 0, 0, 0, DateTimeKind.Local);
+ public static bool IsNewModHalloween
+ {
+ get
+ {
+ var now = DateTime.Now;
+ return now.Date == HalloweenStartNextYear.Date;
+ }
+ }
+ public static bool IsHalloweenSeason
+ {
+ get
+ {
+ var now = DateTime.Now;
+ var start = HalloweenStartThisYear;
+ var seasonStart = start - HalloweenWindow;
+ var seasonEnd = start + HalloweenWindow;
+ return now >= seasonStart && now < seasonEnd;
+ }
+ }
+ public static DateTime UpcomingHalloweenStart
+ {
+ get
+ {
+ var now = DateTime.Now;
+ var startThis = HalloweenStartThisYear;
+ var endThis = startThis + HalloweenWindow;
+ if (now < startThis) return startThis;
+ if (now < endThis) return startThis;
+ return HalloweenStartNextYear;
+ }
+ }
}
}
diff --git a/NewMod/NewModEventHandler.cs b/NewMod/NewModEventHandler.cs
index 507c4d6..93f1b4d 100644
--- a/NewMod/NewModEventHandler.cs
+++ b/NewMod/NewModEventHandler.cs
@@ -1,11 +1,9 @@
+using System;
+using System.Collections;
using System.Collections.Generic;
+using System.Reflection;
+using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
-using MiraAPI.Events.Vanilla.Meeting;
-using MiraAPI.Events.Vanilla.Meeting.Voting;
-using MiraAPI.Events.Vanilla.Usables;
-using NewMod.Patches;
-using NewMod.Patches.Roles.Visionary;
-using NewMod.Roles.ImpostorRoles;
namespace NewMod
{
@@ -13,17 +11,58 @@ public static class NewModEventHandler
{
public static void RegisterEventsLogs()
{
- var registrations = new List
+ var type = typeof(MiraEventManager);
+ var fld = type.GetField("EventWrappers", BindingFlags.NonPublic | BindingFlags.Static);
+ var dictObj = fld.GetValue(null);
+ if (dictObj is not IDictionary dict || dict.Count == 0)
{
- $"{nameof(GameEndEvent)}: {nameof(EndGamePatch.OnGameEnd)}",
- $"{nameof(EnterVentEvent)}: {nameof(VisionaryVentPatch.OnEnterVent)}",
- $"{nameof(BeforeMurderEvent)}: {nameof(VisionaryMurderPatch.OnBeforeMurder)}",
- $"{nameof(AfterMurderEvent)}: {nameof(NewMod.OnAfterMurder)}",
- $"{nameof(HandleVoteEvent)}: {nameof(Tyrant.OnHandleVote)}",
- $"{nameof(StartMeetingEvent)}: {nameof(Tyrant.OnMeetingStart)}",
- $"{nameof(ProcessVotesEvent)}: {nameof(Tyrant.OnProcessVotes)}"
- };
- NewMod.Instance.Log.LogInfo("Registered events: " + "\n" + string.Join(", ", registrations));
+ return;
+ }
+ var sb = new System.Text.StringBuilder();
+ sb.AppendLine("=== Registered NewMod Events ===");
+
+ foreach (DictionaryEntry entry in dict)
+ {
+ var eventType = entry.Key as Type;
+ var listObj = entry.Value;
+ int count = 0;
+ var lines = new List();
+
+ if (listObj is IEnumerable wrappers)
+ {
+ foreach (var wrapper in wrappers)
+ {
+ if (wrapper == null) continue;
+ var wType = wrapper.GetType();
+
+ var ehProp = wType.GetProperty("EventHandler", BindingFlags.Public | BindingFlags.Instance);
+ var prProp = wType.GetProperty("Priority", BindingFlags.Public | BindingFlags.Instance);
+
+ var del = ehProp.GetValue(wrapper) as Delegate;
+ var prio = prProp.GetValue(wrapper) as int? ?? 0;
+
+ var method = del.Method;
+ var declType = method.DeclaringType.FullName;
+ var methodName = method.Name;
+
+ lines.Add($" [{prio}] {declType}.{methodName}()");
+ count++;
+ }
+ }
+
+ sb.AppendLine($"{eventType.FullName} (handlers: {count})");
+ foreach (var l in lines) sb.AppendLine(l);
+ }
+ NewMod.Instance.Log.LogInfo(sb.ToString());
+ }
+
+ // General events
+ [RegisterEvent]
+ public static void OnRoundStart(RoundStartEvent evt)
+ {
+ if (!evt.TriggeredByIntro) return;
+
+ HudManager.Instance.Chat.enabled = false;
}
}
}
diff --git a/NewMod/NewModFaction.cs b/NewMod/NewModFaction.cs
index ee0c6df..c2c0b34 100644
--- a/NewMod/NewModFaction.cs
+++ b/NewMod/NewModFaction.cs
@@ -3,6 +3,7 @@ namespace NewMod
public enum NewModFaction
{
Apex,
- Entropy
+ Entropy,
+ Sentinel
}
}
\ No newline at end of file
diff --git a/NewMod/Options/CompatibilityOptions.cs b/NewMod/Options/CompatibilityOptions.cs
index 063e0ff..cbfcab4 100644
--- a/NewMod/Options/CompatibilityOptions.cs
+++ b/NewMod/Options/CompatibilityOptions.cs
@@ -3,12 +3,35 @@
using MiraAPI.GameOptions.OptionTypes;
namespace NewMod.Options;
+
+#pragma warning disable
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);
- public ModdedEnumOption Compatibility { get; } = new("Mod Compatibility", ModPriority.PreferNewMod);
+ 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,
diff --git a/NewMod/Options/GeneralNpcOptions.cs b/NewMod/Options/GeneralNpcOptions.cs
new file mode 100644
index 0000000..b6d301d
--- /dev/null
+++ b/NewMod/Options/GeneralNpcOptions.cs
@@ -0,0 +1,14 @@
+/*using System;
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.OptionTypes;
+
+namespace NewMod.Options;
+
+public class GeneralNpcOptions : AbstractOptionGroup
+{
+ public override string GroupName => "General NPC";
+ public override Func GroupVisible => () => OptionGroupSingleton.Instance.SpawnNpcAfterRoundStart;
+ public ModdedNumberOption GeneralNPCSpeed { get; } = new("General NPC Speed", min: 1f, max: 5f, increment: 1f, defaultValue: 2f, suffixType: MiraAPI.Utilities.MiraNumberSuffixes.None);
+ public ModdedNumberOption GeneralNPCStopTime { get; } = new("General NPC Stop Time", min: 1f, max: 3f, increment: 1f, defaultValue: 1f, suffixType: MiraAPI.Utilities.MiraNumberSuffixes.None);
+ public ModdedNumberOption GeneralNPCRunTime { get; } = new("General NPC Run Time", min: 1f, max: 10f, increment: 1f, defaultValue: 3f, suffixType: MiraAPI.Utilities.MiraNumberSuffixes.None);
+}*/
\ No newline at end of file
diff --git a/NewMod/Options/GeneralOption.cs b/NewMod/Options/GeneralOption.cs
index fe4ead2..75c17f2 100644
--- a/NewMod/Options/GeneralOption.cs
+++ b/NewMod/Options/GeneralOption.cs
@@ -3,13 +3,34 @@
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("Allows dead players to open cams anywhere")]
+ public bool AllowCams { get; set; } = false;
+
+ [ModdedNumberOption("Total Neutrals", min: 0f, max: 10, 1f)]
+ public float TotalNeutrals { get; set; } = 3f;
+
+ [ModdedToggleOption("Keep Crew Majority")]
+ public bool KeepCrewMajority { get; set; } = true;
+
+ [ModdedToggleOption("Prefer Variety")]
+ public bool PreferVariety { get; set; } = true;
+
+ [ModdedToggleOption("Dead players can see roles in meetings")]
+ public bool ShouldDeadPlayersSeeRoles { get; set; } = true;
+
+ [ModdedToggleOption("Anonymous Names in Meetings")]
+ public bool EnableAnonymousNamesInMeetings { get; set; } = false;
+ public ModdedNumberOption SpawnChanceOfGlitchEffect { get; } = new("Spawn Chance of Glitch Effect", 0f, 0f, 100f, 10f, MiraAPI.Utilities.MiraNumberSuffixes.Percent);
+ public ModdedPlayerOption ChosenPlayer { get; } = new("Player who will receive the effect", true)
+ {
+ Visible = () => OptionGroupSingleton.Instance.SpawnChanceOfGlitchEffect.Value > 0f
+ };
- [ModdedToggleOption("Can Open Cams")]
- public bool CanOpenCams { get; set; } = true;
+ /*[ModdedToggleOption("Should spawn NPC after round start")]
+ public bool SpawnNpcAfterRoundStart { get; set; } = false;*/
}
\ No newline at end of file
diff --git a/NewMod/Options/Roles/AegisOptions.cs b/NewMod/Options/Roles/AegisOptions.cs
new file mode 100644
index 0000000..2227414
--- /dev/null
+++ b/NewMod/Options/Roles/AegisOptions.cs
@@ -0,0 +1,42 @@
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.Attributes;
+using MiraAPI.Utilities;
+using NewMod.Roles.CrewmateRoles;
+
+namespace NewMod.Options.Roles.AegisOptions
+{
+ public class AegisOptions : AbstractOptionGroup
+ {
+ public override string GroupName => "Aegis Options";
+
+ [ModdedNumberOption("Barrier Cooldown", min: 5f, max: 60f, suffixType: MiraNumberSuffixes.Seconds)]
+ public float AegisCooldown { get; set; } = 20f;
+
+ [ModdedNumberOption("Barrier Max Uses", min: 1f, max: 5f, suffixType: MiraNumberSuffixes.None)]
+ public float MaxCharges { get; set; } = 2f;
+
+ [ModdedNumberOption("Barrier Duration", min: 2f, max: 30f, suffixType: MiraNumberSuffixes.Seconds)]
+ public float DurationSeconds { get; set; } = 20f;
+
+ [ModdedNumberOption("Barrier Radius", min: 1f, max: 7f, suffixType: MiraNumberSuffixes.None)]
+ public float Radius { get; set; } = 4f;
+
+ [ModdedEnumOption("Barrier Behavior", typeof(AegisMode))]
+ public AegisMode Behavior { get; set; } = AegisMode.WarnOnly;
+
+ [ModdedEnumOption("Ward Visibility", typeof(WardVisibilityMode))]
+ public WardVisibilityMode Visibility { get; set; } = WardVisibilityMode.OwnerOnly;
+ public enum AegisMode
+ {
+ BlockAndReveal,
+ Block,
+ WarnOnly
+ }
+ public enum WardVisibilityMode
+ {
+ OwnerOnly,
+ TeamOnly,
+ AllPlayers
+ }
+ }
+}
diff --git a/NewMod/Options/Roles/BeaconOptions.cs b/NewMod/Options/Roles/BeaconOptions.cs
new file mode 100644
index 0000000..1729006
--- /dev/null
+++ b/NewMod/Options/Roles/BeaconOptions.cs
@@ -0,0 +1,30 @@
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.Attributes;
+using MiraAPI.Utilities;
+using NewMod.Roles.CrewmateRoles;
+
+namespace NewMod.Options.Roles.BeaconOptions
+{
+ public class BeaconOptions : AbstractOptionGroup
+ {
+ public override string GroupName => "Beacon Options";
+
+ [ModdedNumberOption("Tasks per Charge", min: 1f, max: 6f, suffixType: MiraNumberSuffixes.None)]
+ public float TasksPerCharge { get; set; } = 2f;
+
+ [ModdedNumberOption("Max Charges", min: 1f, max: 6f, suffixType: MiraNumberSuffixes.None)]
+ public float MaxCharges { get; set; } = 3f;
+
+ [ModdedNumberOption("Pulse Duration", min: 1f, max: 30f, suffixType: MiraNumberSuffixes.Seconds)]
+ public float PulseDuration { get; set; } = 15f;
+
+ [ModdedNumberOption("Pulse Cooldown", min: 0f, max: 60f, suffixType: MiraNumberSuffixes.Seconds)]
+ public float PulseCooldown { get; set; } = 15f;
+
+ [ModdedToggleOption("Show Live Counts During Pulse")]
+ public bool ShowOnMinimap { get; set; } = true;
+
+ [ModdedToggleOption("Include Dead Bodies")]
+ public bool IncludeDeadBodies { get; set; } = false;
+ }
+}
diff --git a/NewMod/Options/Roles/EdgeveilOptions.cs b/NewMod/Options/Roles/EdgeveilOptions.cs
new file mode 100644
index 0000000..225503e
--- /dev/null
+++ b/NewMod/Options/Roles/EdgeveilOptions.cs
@@ -0,0 +1,32 @@
+using MiraAPI.GameOptions;
+using MiraAPI.GameOptions.Attributes;
+using MiraAPI.GameOptions.OptionTypes;
+using MiraAPI.Utilities;
+using NewMod.Roles.ImpostorRoles;
+
+namespace NewMod.Options.Roles.EdgeveilOptions
+{
+ public class EdgeveilOptions : AbstractOptionGroup
+ {
+ public override string GroupName => "Edgeveil Settings";
+
+ [ModdedNumberOption("Slash Cooldown", min: 15f, max: 60f, increment: 1f, suffixType: MiraNumberSuffixes.Seconds)]
+ public float SlashCooldown { get; set; } = 20f;
+
+ [ModdedNumberOption("Slash Max Uses", min: 1f, max: 3f, increment: 1f, suffixType: MiraNumberSuffixes.Seconds)]
+ public float SlashMaxUses { get; set; } = 1f;
+
+ [ModdedNumberOption("Slash Range", min: 1f, max: 6f, increment: 1f, suffixType: MiraNumberSuffixes.None)]
+ public float SlashRange { get; set; } = 3f;
+
+ [ModdedNumberOption("Slash Tray Speed", min: 1f, max: 6f, increment: 1f, suffixType: MiraNumberSuffixes.None)]
+ public float SlashSpeed { get; set; } = 3f;
+
+ [ModdedNumberOption("Duration of Shake Effect", min: 5f, max: 6f, increment: 1f, suffixType: MiraNumberSuffixes.None)]
+ public float EffectDuration { get; set; } = 3f;
+
+ [ModdedNumberOption("Max players the Arc can kill", min: 1f, max: 6f, increment: 1f, suffixType: MiraNumberSuffixes.None)]
+ public float PlayersToKill { get; set; } = 4f;
+
+ }
+}
diff --git a/NewMod/Options/Roles/EgoistOptions/EgoistOptions.cs b/NewMod/Options/Roles/EgoistOptions.cs
similarity index 94%
rename from NewMod/Options/Roles/EgoistOptions/EgoistOptions.cs
rename to NewMod/Options/Roles/EgoistOptions.cs
index 5781ba2..b2535e8 100644
--- a/NewMod/Options/Roles/EgoistOptions/EgoistOptions.cs
+++ b/NewMod/Options/Roles/EgoistOptions.cs
@@ -1,5 +1,4 @@
using MiraAPI.GameOptions;
-using MiraAPI.GameOptions.Attributes;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Utilities;
using NewMod.Roles.NeutralRoles;
diff --git a/NewMod/Options/Roles/EnergyThiefOptions/EnergyThiefOptions.cs b/NewMod/Options/Roles/EnergyThiefOptions.cs
similarity index 100%
rename from NewMod/Options/Roles/EnergyThiefOptions/EnergyThiefOptions.cs
rename to NewMod/Options/Roles/EnergyThiefOptions.cs
diff --git a/NewMod/Options/Roles/InjectorOptions/InjectorOptions.cs b/NewMod/Options/Roles/InjectorOptions.cs
similarity index 100%
rename from NewMod/Options/Roles/InjectorOptions/InjectorOptions.cs
rename to NewMod/Options/Roles/InjectorOptions.cs
diff --git a/NewMod/Options/Roles/NecromancerOptions/NecromancerRoleOption.cs b/NewMod/Options/Roles/NecromancerRoleOption.cs
similarity index 100%
rename from NewMod/Options/Roles/NecromancerOptions/NecromancerRoleOption.cs
rename to NewMod/Options/Roles/NecromancerRoleOption.cs
diff --git a/NewMod/Options/Roles/OverloadOptions/OverloadOptions.cs b/NewMod/Options/Roles/OverloadOptions.cs
similarity index 100%
rename from NewMod/Options/Roles/OverloadOptions/OverloadOptions.cs
rename to NewMod/Options/Roles/OverloadOptions.cs
diff --git a/NewMod/Options/Roles/PranksterOptions/PranksterOptions.cs b/NewMod/Options/Roles/PranksterOptions.cs
similarity index 100%
rename from NewMod/Options/Roles/PranksterOptions/PranksterOptions.cs
rename to NewMod/Options/Roles/PranksterOptions.cs
diff --git a/NewMod/Options/Roles/PulseBladeOptions/PulseBladeOptions.cs b/NewMod/Options/Roles/PulseBladeOptions.cs
similarity index 100%
rename from NewMod/Options/Roles/PulseBladeOptions/PulseBladeOptions.cs
rename to NewMod/Options/Roles/PulseBladeOptions.cs
diff --git a/NewMod/Options/Roles/RevenantOptions/RevenantOptions.cs b/NewMod/Options/Roles/RevenantOptions.cs
similarity index 100%
rename from NewMod/Options/Roles/RevenantOptions/RevenantOptions.cs
rename to NewMod/Options/Roles/RevenantOptions.cs
diff --git a/NewMod/Options/Roles/SpecialAgentOptions/SpecialAgentOptions.cs b/NewMod/Options/Roles/SpecialAgentOptions.cs
similarity index 100%
rename from NewMod/Options/Roles/SpecialAgentOptions/SpecialAgentOptions.cs
rename to NewMod/Options/Roles/SpecialAgentOptions.cs
diff --git a/NewMod/Options/Roles/TheVisionaryOptions/TheVisionaryOptions.cs b/NewMod/Options/Roles/TheVisionaryOptions.cs
similarity index 100%
rename from NewMod/Options/Roles/TheVisionaryOptions/TheVisionaryOptions.cs
rename to NewMod/Options/Roles/TheVisionaryOptions.cs
diff --git a/NewMod/Options/Roles/TyrantOptions/TyrantOptions.cs b/NewMod/Options/Roles/TyrantOptions.cs
similarity index 100%
rename from NewMod/Options/Roles/TyrantOptions/TyrantOptions.cs
rename to NewMod/Options/Roles/TyrantOptions.cs
diff --git a/NewMod/Options/Roles/WraithCallerOptions/WraithCallerOptions.cs b/NewMod/Options/Roles/WraithCallerOptions.cs
similarity index 77%
rename from NewMod/Options/Roles/WraithCallerOptions/WraithCallerOptions.cs
rename to NewMod/Options/Roles/WraithCallerOptions.cs
index ea85122..9c71447 100644
--- a/NewMod/Options/Roles/WraithCallerOptions/WraithCallerOptions.cs
+++ b/NewMod/Options/Roles/WraithCallerOptions.cs
@@ -18,7 +18,13 @@ public class WraithCallerOptions : AbstractOptionGroup
[ModdedNumberOption("Required NPCs to Send", min: 1, max: 5)]
public float RequiredNPCsToSend { get; set; } = 2f;
+ [ModdedNumberOption("NPC Speed", min: 1, max: 5, increment: 1)]
+ public float NPCSpeed { get; set; } = 1f;
+
[ModdedToggleOption("Show Summon Warnings")]
public bool ShowSummonWarnings { get; set; } = true;
+
+ [ModdedToggleOption("Switch cam to NPC on target send")]
+ public bool ShouldSwitchCamToNPC { get; set; } = true;
}
}
diff --git a/NewMod/Patches/Birthday/LobbyPatch.cs b/NewMod/Patches/Birthday/LobbyPatch.cs
index c1e5790..effe317 100644
--- a/NewMod/Patches/Birthday/LobbyPatch.cs
+++ b/NewMod/Patches/Birthday/LobbyPatch.cs
@@ -1,9 +1,11 @@
using UnityEngine;
using HarmonyLib;
-using System;
using Object = UnityEngine.Object;
using Reactor.Utilities.Extensions;
-using System.IO;
+using MiraAPI.Utilities;
+using NewMod.Components.ScreenEffects;
+using Reactor.Utilities;
+using NewMod.Utilities;
namespace NewMod.Patches.Birthday
{
@@ -11,7 +13,7 @@ namespace NewMod.Patches.Birthday
public static class LobbyPatch
{
public static GameObject CustomLobby;
- public static Toast ToastObj;
+ public static BirthdayToast ToastObj;
public static readonly Vector2[] BirthdaySpawns =
[
new Vector2(-0.6738f, -2.5016f),
@@ -29,24 +31,62 @@ public static class LobbyPatch
[HarmonyPrefix]
public static bool StartPrefix(LobbyBehaviour __instance)
{
- CustomLobby = Object.Instantiate(NewModAsset.CustomLobby.LoadAsset());
- CustomLobby.transform.SetParent(__instance.transform, false);
- CustomLobby.transform.localPosition = Vector3.zero;
- return true;
+ if (NewModDateTime.IsNewModBirthdayWeek)
+ {
+ __instance.SpawnPositions = new Vector2[BirthdaySpawns.Length];
+
+ for (int i = 0; i < BirthdaySpawns.Length; i++)
+ {
+ __instance.SpawnPositions[i] = BirthdaySpawns[i % BirthdaySpawns.Length];
+ }
+ CustomLobby = Object.Instantiate(NewModAsset.HalloweenLobby.LoadAsset());
+ CustomLobby.layer = LayerMask.NameToLayer("Ship");
+ CustomLobby.transform.SetParent(__instance.transform, false);
+ CustomLobby.transform.localPosition = Vector3.zero;
+ return false;
+ }
+ else if (NewModDateTime.IsHalloweenSeason)
+ {
+ __instance.SpawnPositions = new Vector2[BirthdaySpawns.Length];
+
+ for (int i = 0; i < BirthdaySpawns.Length; i++)
+ {
+ __instance.SpawnPositions[i] = BirthdaySpawns[i % BirthdaySpawns.Length];
+ }
+ CustomLobby = Object.Instantiate(NewModAsset.HalloweenLobby.LoadAsset());
+ CustomLobby.layer = LayerMask.NameToLayer("Ship");
+ CustomLobby.transform.SetParent(__instance.transform, false);
+ CustomLobby.transform.localPosition = Vector3.zero;
+
+ if (Helpers.CheckChance(30))
+ {
+ int effectIndex = Random.Range(0, 3);
+ switch (effectIndex)
+ {
+ case 0:
+ Camera.main.gameObject.AddComponent();
+ break;
+ case 1:
+ Camera.main.gameObject.AddComponent();
+ break;
+ case 2:
+ Camera.main.gameObject.AddComponent();
+ break;
+ }
+ }
+ return false;
+ }
+ else
+ {
+ return true;
+ }
}
[HarmonyPatch(nameof(LobbyBehaviour.Start))]
[HarmonyPostfix]
public static void Postfix(LobbyBehaviour __instance)
{
- ToastObj = Toast.CreateToast();
- ToastObj.transform.localPosition = new Vector3(-4.4217f, 2.2098f, 0f);
-
- if (DateTime.Now < NewModDateTime.NewModBirthday)
- {
- TimeSpan countdown = NewModDateTime.NewModBirthday - DateTime.Now;
- ToastObj.StartCountdown(countdown);
- }
+ if (!NewModDateTime.IsNewModBirthdayWeek || !NewModDateTime.IsHalloweenSeason) return;
var originalLobby = "Lobby(Clone)";
GameObject.Find(originalLobby).GetComponent().Destroy();
@@ -61,21 +101,16 @@ public static void Postfix(LobbyBehaviour __instance)
var wardrobe = GameObject.Find(originalLobby + "/panel_Wardrobe");
if (wardrobe != null)
{
- wardrobe.transform.localPosition = new Vector3(4.6701f, -0.0529f, 0f);
- wardrobe.transform.localScale = new Vector3(0.7301f, 0.7f, 1f);
- }
- __instance.SpawnPositions = new Vector2[BirthdaySpawns.Length];
-
- for (int i = 0; i < BirthdaySpawns.Length; i++)
- {
- __instance.SpawnPositions[i] = BirthdaySpawns[i];
+ wardrobe.transform.localPosition = new Vector3(-4.368f, -0.0027f, 0f);
+ wardrobe.transform.localScale = new Vector3(0.6475f, 0.7f, 1f);
}
}
+
[HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Start))]
public static void Prefix(ShipStatus __instance)
{
CustomLobby.DestroyImmediate();
- ToastObj.gameObject.SetActive(false);
+ Coroutines.Start(CoroutinesHelper.RemoveCameraEffect(Camera.main, 0f));
}
}
}
diff --git a/NewMod/Patches/CustomPlayerTagPatch.cs b/NewMod/Patches/CustomPlayerTagPatch.cs
deleted file mode 100644
index 6ddfa37..0000000
--- a/NewMod/Patches/CustomPlayerTagPatch.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using HarmonyLib;
-using UnityEngine;
-
-namespace NewMod.Patches
-{
- [HarmonyPatch]
- public static class CustomPlayerTagPatch
- {
- public const float Padding = 0.02f;
- private static float targetY;
-
- [HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetName))]
- [HarmonyPostfix]
- public static void SetNamePostfix(ChatBubble __instance, string playerName, bool isDead, bool voted, Color color)
- {
- __instance.NameText.ForceMeshUpdate();
-
- float nameBottom = __instance.NameText.textBounds.min.y;
- float nameLocalY = __instance.NameText.transform.localPosition.y;
-
- targetY = nameLocalY + nameBottom - Padding;
- }
-
- [HarmonyPatch(typeof(ChatBubble), nameof(ChatBubble.SetText))]
- [HarmonyPostfix]
- public static void SetTextPostfix(ChatBubble __instance, string chatText)
- {
- var pos = __instance.TextArea.transform.localPosition;
- pos.y = targetY;
-
- __instance.TextArea.transform.localPosition = pos;
- __instance.AlignChildren();
- }
- [HarmonyPatch(typeof(NotificationPopper), nameof(NotificationPopper.AddDisconnectMessage))]
- [HarmonyPrefix]
- public static void StartPrefix(NotificationPopper __instance, ref string item)
- {
- item = item.Replace("\r", "").Replace("\n", "");
- while (item.Contains(" ")) item = item.Replace(" ", " ");
-
- item = item.Replace("", "");
- item = item.Replace("", "");
-
- _ = item.TrimEnd();
- }
- [HarmonyPatch(typeof(ChatNotification), nameof(ChatNotification.SetUp))]
- [HarmonyPrefix]
- public static void StartPrefix(ChatNotification __instance, PlayerControl sender, ref string text)
- {
- if (text.Contains('\n'))
- text = text.Replace("\r", "").Replace("\n", " ");
-
- text = text.Replace("", "");
-
- while (text.Contains(" "))
- text = text.Replace(" ", " ");
-
- text = text.TrimEnd();
- }
- }
-}
diff --git a/NewMod/Patches/EndGamePatch.cs b/NewMod/Patches/EndGamePatch.cs
index e27777c..3ca6b59 100644
--- a/NewMod/Patches/EndGamePatch.cs
+++ b/NewMod/Patches/EndGamePatch.cs
@@ -216,6 +216,7 @@ public static class CheckGameEndPatch
public static bool Prefix(ShipStatus __instance)
{
if (DestroyableSingleton.InstanceExists) return true;
+ if (Time.timeSinceLevelLoad < 2f) return true;
if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.WraithCallerWin)) return false;
if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.PulseBladeWin)) return false;
if (CheckForEndGameFaction(__instance, (GameOverReason)NewModEndReasons.TyrantWin)) return false;
@@ -244,7 +245,7 @@ public static bool CheckForEndGameFaction(ShipStatus __instance, GameO
var alives = Helpers.GetAlivePlayers();
- if (alives.Count > playersThreshold) continue;
+ if (alives.Count >= playersThreshold) continue;
int strikes = Utils.GetStrikes(player.PlayerId);
if (strikes >= requiredStrikes)
diff --git a/NewMod/Patches/GameOptionsMenu.cs b/NewMod/Patches/GameOptionsMenu.cs
deleted file mode 100644
index 90ff21d..0000000
--- a/NewMod/Patches/GameOptionsMenu.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using HarmonyLib;
-using MiraAPI.GameOptions;
-using NewMod.Options;
-
-namespace NewMod.Patches;
-
-[HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CloseMenu))]
-public static class GameOptionsMenu_CloseMenu_Patch
-{
- public static void Postfix(GameOptionsMenu __instance)
- {
- var opts = OptionGroupSingleton.Instance;
-
- if (opts.AllowRevenantHitmanCombo.Value)
- {
- HudManager.Instance.ShowPopUp(
- "You enabled the Revenant & Hitman combo. This may break game balance!"
- );
- }
- else
- {
- HudManager.Instance.ShowPopUp(
- "Revenant & Hitman combo disabled. Only one will be allowed per match."
- );
- }
- }
-}
diff --git a/NewMod/Patches/MainMenuPatch.cs b/NewMod/Patches/MainMenuPatch.cs
index a0ec854..8684b7c 100644
--- a/NewMod/Patches/MainMenuPatch.cs
+++ b/NewMod/Patches/MainMenuPatch.cs
@@ -38,13 +38,23 @@ public static void StartPostfix(MainMenuManager __instance)
RightPanel = __instance.transform.Find("MainUI/AspectScaler/RightPanel");
- if (!_wraithRegistered)
+ if (NewModDateTime.IsWraithCallerUnlocked && !_wraithRegistered)
{
- RegisterWraithCaller();
_wraithRegistered = true;
}
- Coroutines.Start(ApplyBirthdayUI(__instance));
+ if (NewModDateTime.IsNewModBirthdayWeek)
+ {
+ Coroutines.Start(ApplyBirthdayUI(__instance));
+ }
+ else
+ {
+ var Logo = new GameObject("NewModLogo");
+ Logo.transform.SetParent(__instance.transform.Find("MainCanvas/MainPanel/RightPanel"), false);
+ Logo.transform.localPosition = new Vector3(2.34f, -0.7136f, 1f);
+ LogoSprite = Logo.AddComponent();
+ LogoSprite.sprite = NewModAsset.ModLogo.LoadAsset();
+ }
ModCompatibility.Initialize();
}
@@ -82,19 +92,7 @@ private static IEnumerator ApplyBirthdayUI(MainMenuManager __instance)
if (auBG != null && bg != null) auBG.sprite = bg;
}
}
-
- public static void RegisterWraithCaller()
- {
- var roleType = typeof(WraithCaller);
- var customRoleManager = typeof(CustomRoleManager);
- var registerTypes = customRoleManager.GetMethod("RegisterRoleTypes", BindingFlags.NonPublic | BindingFlags.Static);
- var registerInManager = customRoleManager.GetMethod("RegisterInRoleManager", BindingFlags.NonPublic | BindingFlags.Static);
- var plugin = MiraPluginManager.GetPluginByGuid(NewMod.Id);
- registerTypes.Invoke(null, new object[] { new List { roleType }, plugin });
- registerInManager.Invoke(null, null);
- }
-
- [HarmonyPatch(nameof(MainMenuManager.OpenGameModeMenu))]
+ /*[HarmonyPatch(nameof(MainMenuManager.OpenGameModeMenu))]
[HarmonyPatch(nameof(MainMenuManager.OpenCredits))]
[HarmonyPatch(nameof(MainMenuManager.OpenAccountMenu))]
[HarmonyPatch(nameof(MainMenuManager.OpenCreateGame))]
@@ -103,14 +101,17 @@ public static void RegisterWraithCaller()
[HarmonyPatch(nameof(MainMenuManager.OpenFindGame))]
public static void Postfix(MainMenuManager __instance)
{
- if (RightPanel != null) RightPanel.gameObject.SetActive(true);
+ if (!NewModDateTime.IsNewModBirthdayWeek) return;
+ RightPanel.gameObject.SetActive(true);
}
[HarmonyPatch(nameof(MainMenuManager.ResetScreen))]
[HarmonyPostfix]
public static void ResetScreenPostfix(MainMenuManager __instance)
{
- if (RightPanel != null) RightPanel.gameObject.SetActive(false);
+ if (!NewModDateTime.IsNewModBirthdayWeek) return;
+ RightPanel.gameObject.SetActive(false);
}
+ }*/
}
-}
+}
\ No newline at end of file
diff --git a/NewMod/Patches/Roles/Beacon/ShowMapPatch.cs b/NewMod/Patches/Roles/Beacon/ShowMapPatch.cs
new file mode 100644
index 0000000..4fb2654
--- /dev/null
+++ b/NewMod/Patches/Roles/Beacon/ShowMapPatch.cs
@@ -0,0 +1,125 @@
+using System.Collections;
+using System.Collections.Generic;
+using HarmonyLib;
+using MiraAPI.GameOptions;
+using MiraAPI.Utilities;
+using NewMod.Options.Roles.BeaconOptions;
+using Reactor.Utilities;
+using UnityEngine;
+using BC = NewMod.Roles.CrewmateRoles.Beacon;
+
+namespace NewMod.Patches.Roles.Beacon
+{
+ [HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.Show))]
+ public static class BeaconShowMapPatch
+ {
+ private static readonly List _markers = new();
+ private static bool _armedPulseThisOpen;
+
+ public static void Prefix(MapBehaviour __instance, MapOptions opts)
+ {
+ _armedPulseThisOpen = false;
+
+ if (PlayerControl.LocalPlayer.Data.Role is not BC) return;
+ if (__instance.IsOpen || opts.Mode != MapOptions.Modes.Normal) return;
+
+ var settings = OptionGroupSingleton.Instance;
+
+ if (BC.charges <= 0 || Time.time < BC.cooldownUntil) return;
+
+ BC.charges--;
+ BC.pulseUntil = Time.time + settings.PulseDuration;
+ BC.cooldownUntil = Time.time + settings.PulseCooldown;
+
+ opts.Mode = MapOptions.Modes.CountOverlay;
+ opts.ShowLivePlayerPosition = true;
+ opts.IncludeDeadBodies = settings.IncludeDeadBodies;
+ opts.AllowMovementWhileMapOpen = true;
+
+ _armedPulseThisOpen = true;
+ }
+
+ public static void Postfix(MapBehaviour __instance, MapOptions opts)
+ {
+ if (!_armedPulseThisOpen) return;
+ if (!__instance || !__instance.IsOpen) { _armedPulseThisOpen = false; return; }
+
+ ClearMarkers();
+
+ foreach (var pc in PlayerControl.AllPlayerControls)
+ {
+ if (!pc) continue;
+
+ var marker = Object.Instantiate(__instance.HerePoint, __instance.HerePoint.transform.parent);
+ marker.name = $"BeaconMarker_{pc.PlayerId}";
+ marker.enabled = true;
+
+ pc.SetPlayerMaterialColors(marker);
+
+ _markers.Add(marker);
+ }
+
+ Coroutines.Start(CoUpdateMarkers(__instance));
+ _armedPulseThisOpen = false;
+
+ Helpers.CreateAndShowNotification(
+ $"Beacon pulse active ({BC.charges}/{OptionGroupSingleton.Instance.MaxCharges} left)",
+ new Color(0.75f, 0.65f, 1f),
+ spr: NewModAsset.RadarIcon.LoadAsset());
+ }
+ [HarmonyPatch(typeof(MapCountOverlay), nameof(MapCountOverlay.Update))]
+ public static class BeaconOverlayTintKeeper
+ {
+ static void Postfix(MapCountOverlay __instance)
+ {
+ if (PlayerControl.LocalPlayer.Data.Role is not BC) return;
+ if (Time.time >= BC.pulseUntil) return;
+
+ var map = MapBehaviour.Instance;
+ if (!map || !map.IsOpen) return;
+ if (!__instance || !__instance.isActiveAndEnabled) return;
+
+ if (__instance.BackgroundColor)
+ {
+ __instance.BackgroundColor.SetColor(new Color(0.60f, 0.20f, 0.80f, 1f));
+ }
+ }
+ }
+
+ public static IEnumerator CoUpdateMarkers(MapBehaviour map)
+ {
+ while (Time.time < BC.pulseUntil && map && map.IsOpen && ShipStatus.Instance)
+ {
+ var players = PlayerControl.AllPlayerControls.ToArray();
+ for (int i = 0; i < players.Length && i < _markers.Count; i++)
+ {
+ var pc = players[i];
+ var mrk = _markers[i];
+ if (!pc || !mrk) continue;
+
+ Vector3 v = pc.transform.position;
+ v /= ShipStatus.Instance.MapScale;
+ v.x *= Mathf.Sign(ShipStatus.Instance.transform.localScale.x);
+ v.z = -1f;
+ mrk.transform.localPosition = v;
+ }
+ yield return null;
+ }
+
+ ClearMarkers();
+ if (map && map.IsOpen)
+ map.Show(new MapOptions { Mode = MapOptions.Modes.Normal });
+ }
+
+ [HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.Close))]
+ public static class BeaconCloseMapPatch
+ {
+ public static void Postfix() => ClearMarkers();
+ }
+ public static void ClearMarkers()
+ {
+ foreach (var r in _markers) if (r) Object.Destroy(r.gameObject);
+ _markers.Clear();
+ }
+ }
+}
diff --git a/NewMod/Patches/SelectRolePatch.cs b/NewMod/Patches/SelectRolePatch.cs
new file mode 100644
index 0000000..b41fb8b
--- /dev/null
+++ b/NewMod/Patches/SelectRolePatch.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AmongUs.GameOptions;
+using HarmonyLib;
+using MiraAPI.GameOptions;
+using MiraAPI.Roles;
+using MiraAPI.Utilities;
+using NewMod;
+using NewMod.Options;
+using NewMod.Roles;
+using Reactor.Utilities;
+using UnityEngine;
+
+namespace NewMod.Patches
+{
+ [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SelectRoles))]
+ public static class SelectRolePatch
+ {
+ public static void Postfix(RoleManager __instance)
+ {
+ if (!AmongUsClient.Instance.AmHost) return;
+
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: START --------------");
+ Logger.Instance.LogMessage(
+ $"SelectRoles Postfix entered on {(AmongUsClient.Instance.AmHost ? "HOST" : "CLIENT")} (clientId={AmongUsClient.Instance.ClientId})");
+
+
+ var opts = OptionGroupSingleton.Instance;
+ int target = Mathf.RoundToInt(opts.TotalNeutrals);
+ Logger.Instance.LogMessage($"Config -> TotalNeutrals={opts.TotalNeutrals} (target={target}), KeepCrewMajority={opts.KeepCrewMajority}, PreferVariety={opts.PreferVariety}");
+
+ var all = GameData.Instance.AllPlayers.ToArray()
+ .Where(p => !p.IsDead && !p.Disconnected && p.Object)
+ .ToList();
+ Logger.Instance.LogMessage($"Alive players eligible (all): {all.Count}");
+
+ var neutrals = all.Where(p =>
+ {
+ var rb = p.Object.Data.Role;
+ return rb is ICustomRole cr && cr is INewModRole nm &&
+ (nm.Faction == NewModFaction.Apex || nm.Faction == NewModFaction.Entropy);
+ }).Select(p => p.Object).ToList();
+ Logger.Instance.LogMessage($"Current neutrals (Apex or Entropy): {neutrals.Count}");
+
+ if (opts.KeepCrewMajority)
+ {
+ int crewCount = all.Count(p =>
+ {
+ var rb = p.Object.Data.Role;
+ if (!rb) return false;
+ return (rb is CrewmateRole) || (!rb.IsImpostor && rb.TeamType == RoleTeamTypes.Crewmate);
+ });
+
+ int maxAllowed = Math.Max(0, (int)Math.Floor((crewCount - 1) / 2.0));
+ int before = target;
+ target = Math.Min(target, maxAllowed);
+ Logger.Instance.LogMessage($"KeepCrewMajority -> crewCount={crewCount}, maxAllowedNeutrals={maxAllowed}, target {before} => {target}");
+ }
+
+ int have = neutrals.Count;
+ Logger.Instance.LogMessage($"Neutral count check -> have={have}, target={target}");
+
+ if (have == target)
+ {
+ Logger.Instance.LogMessage("No changes needed, exiting early.");
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: END (no-op) --------------");
+ return;
+ }
+
+ if (have > target)
+ {
+ int remove = have - target;
+ Logger.Instance.LogMessage($"Too many neutrals -> remove={remove}. Shuffling current neutrals...");
+ neutrals.Shuffle();
+
+ for (int i = 0; i < remove && i < neutrals.Count; i++)
+ {
+ var ply = neutrals[i];
+ Logger.Instance.LogMessage($"Demoting to Crewmate -> {ply.PlayerId}");
+ ply.RpcSetRole(RoleTypes.Crewmate);
+ }
+
+ Logger.Instance.LogMessage("Demotion phase complete.");
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: END (demotions) --------------");
+ return;
+ }
+
+ int need = target - have;
+ Logger.Instance.LogMessage($"Need more neutrals -> need={need}");
+
+ var crewElig = all.Where(p =>
+ {
+ var rb = p.Object.Data.Role;
+ if (!rb) return false;
+ bool isCrew = (rb is CrewmateRole) || (!rb.IsImpostor && rb.TeamType == RoleTeamTypes.Crewmate);
+ if (!isCrew) return false;
+
+ if (rb is ICustomRole cr && cr is INewModRole nm2)
+ return nm2.Faction != NewModFaction.Apex && nm2.Faction != NewModFaction.Entropy;
+
+ return true;
+ }).Select(p => p.Object).ToList();
+ Logger.Instance.LogMessage($"Crew eligible for conversion -> count={crewElig.Count}");
+
+ if (crewElig.Count == 0)
+ {
+ Logger.Instance.LogMessage("No crew eligible to convert. Exiting.");
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: END (no elig crew) --------------");
+ return;
+ }
+
+ var active = CustomRoleUtils.GetActiveRoles().ToList();
+ Logger.Instance.LogMessage($"Active custom roles snapshot -> count={active.Count}");
+
+ var candidates = new List();
+
+ foreach (var r in CustomRoleManager.CustomMiraRoles)
+ {
+ if (r is not INewModRole nm) continue;
+ if (nm.Faction != NewModFaction.Apex && nm.Faction != NewModFaction.Entropy) continue;
+
+ var roleType = (RoleTypes)RoleId.Get(r.GetType());
+ int already = active.Count(x => x && x.Role == roleType);
+ int left = r.Configuration.MaxRoleCount - already;
+ if (left <= 0) continue;
+
+ int chance = r.GetChance() ?? r.Configuration.DefaultChance;
+ float weight = chance;
+ if (weight <= 0f) continue;
+
+ candidates.Add(new Candidate { Role = r, Left = left, Weight = weight, RoleType = roleType });
+ Logger.Instance.LogMessage($"Candidate -> {r.GetType().Name} type={(ushort)roleType} left={left} weight={weight} already={already} max={r.Configuration.MaxRoleCount}");
+ }
+
+ Logger.Instance.LogMessage($"Candidate pool built -> count={candidates.Count}");
+ if (candidates.Count == 0)
+ {
+ Logger.Instance.LogMessage("No candidates to assign. Exiting.");
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: END (no candidates) --------------");
+ return;
+ }
+
+ var picks = new List();
+ if (opts.PreferVariety)
+ {
+ Logger.Instance.LogMessage("PreferVariety enabled -> taking one of each highest weight until need is met.");
+ var ordered = candidates.OrderByDescending(x => x.Weight).ToList();
+ for (int i = 0; i < ordered.Count && picks.Count < need; i++)
+ {
+ if (ordered[i].Left <= 0) continue;
+ picks.Add(ordered[i].Role);
+ Logger.Instance.LogMessage($"Variety pick -> {ordered[i].Role.GetType().Name}");
+ var e = ordered[i]; e.Left -= 1; ordered[i] = e;
+ }
+ candidates = ordered;
+ }
+
+ while (picks.Count < need)
+ {
+ var avail = candidates.Where(c => c.Left > 0 && c.Weight > 0f).ToList();
+ if (avail.Count == 0)
+ {
+ Logger.Instance.LogMessage("No more available candidates with slots and weight. Breaking.");
+ break;
+ }
+
+ float total = avail.Sum(c => c.Weight);
+ float rnum = UnityEngine.Random.Range(0f, total);
+ float acc = 0f;
+ var chosen = avail[0];
+
+ for (int i = 0; i < avail.Count; i++)
+ {
+ acc += avail[i].Weight;
+ if (rnum <= acc) { chosen = avail[i]; break; }
+ }
+
+ picks.Add(chosen.Role);
+ Logger.Instance.LogMessage($"Weighted pick -> {chosen.Role.GetType().Name} (roll={rnum:F2} / total={total:F2})");
+
+ int gi = candidates.FindIndex(x => x.RoleType == chosen.RoleType);
+ if (gi >= 0)
+ {
+ candidates[gi] = new Candidate
+ {
+ Role = candidates[gi].Role,
+ Left = candidates[gi].Left - 1,
+ Weight = candidates[gi].Weight,
+ RoleType = candidates[gi].RoleType
+ };
+ Logger.Instance.LogMessage($"Decrement slot -> {candidates[gi].Role.GetType().Name} now left={candidates[gi].Left}");
+ }
+ }
+
+ Logger.Instance.LogMessage($"Final picks -> count={picks.Count}. Starting assignment to crewElig={crewElig.Count}");
+
+ for (int i = 0; i < picks.Count && crewElig.Count > 0; i++)
+ {
+ int idx = HashRandom.FastNext(crewElig.Count);
+ var pc = crewElig[idx];
+ crewElig.RemoveAt(idx);
+
+ var rt = (RoleTypes)RoleId.Get(picks[i].GetType());
+ Logger.Instance.LogMessage($"Assign -> playerId={pc.PlayerId} role={(ushort)rt} ({picks[i].GetType().Name})");
+ pc.RpcSetRole(rt);
+ }
+
+ Logger.Instance.LogMessage("Assignment phase complete.");
+ Logger.Instance.LogMessage("-------------- SELECT ROLES: END --------------");
+ }
+ struct Candidate
+ {
+ public ICustomRole Role;
+ public int Left;
+ public float Weight;
+ public RoleTypes RoleType;
+ }
+ }
+}
diff --git a/NewMod/Patches/SetCosmeticsPatch.cs b/NewMod/Patches/SetCosmeticsPatch.cs
new file mode 100644
index 0000000..215e43d
--- /dev/null
+++ b/NewMod/Patches/SetCosmeticsPatch.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Linq;
+using HarmonyLib;
+using MiraAPI.GameOptions;
+using MiraAPI.Roles;
+using MiraAPI.Utilities;
+using NewMod.Options;
+using NewMod.Roles;
+using NewMod.Utilities;
+using UnityEngine;
+
+namespace NewMod.Patches
+{
+ [HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.SetCosmetics))]
+ public static class PlayerVoteArea_SetCosmetics_Patch
+ {
+ public static Dictionary _alias;
+ public static HashSet _used;
+ public static void Postfix(PlayerVoteArea __instance, NetworkedPlayerInfo playerInfo)
+ {
+ var opts = OptionGroupSingleton.Instance;
+ var lp = PlayerControl.LocalPlayer;
+ bool revealRolesForDead = opts.ShouldDeadPlayersSeeRoles && lp?.Data?.IsDead == true;
+ bool anonNames = opts.EnableAnonymousNamesInMeetings;
+ bool anonIcons = anonNames;
+
+ _alias ??= [];
+ _used ??= [];
+
+ byte playerId = playerInfo.PlayerId;
+ string baseName = playerInfo.PlayerName;
+
+ if (anonNames && !(playerInfo.Object?.notRealPlayer ?? true) && !(playerInfo.Object?.isDummy ?? true))
+ {
+ if (!_alias.TryGetValue(playerId, out var code))
+ {
+ code = Helpers.RandomString(5);
+ while (!_used.Add(code)) code = Helpers.RandomString(5);
+ _alias[playerId] = code;
+ }
+ baseName = _alias[playerId];
+ }
+
+ if (anonIcons && __instance.PlayerIcon != null)
+ {
+ int randomColor = Random.Range(0, Palette.PlayerColors.Length);
+
+ __instance.PlayerIcon.SetBodyColor(randomColor);
+ __instance.PlayerIcon.SetHat("hat_Nohat", 0);
+ __instance.PlayerIcon.SetSkin("", randomColor);
+ __instance.PlayerIcon.SetVisor("", randomColor);
+ }
+
+ if (revealRolesForDead)
+ {
+ var role = playerInfo.Role;
+ string roleText;
+ string hex;
+
+ if (role != null && CustomRoleManager.GetCustomRoleBehaviour(role.Role, out ICustomRole customRole))
+ {
+ roleText = customRole is INewModRole nm
+ ? $"{nm.RoleName} [{Utils.GetFactionDisplay(nm)}]"
+ : customRole.RoleName;
+ hex = ColorUtility.ToHtmlStringRGB(customRole.RoleColor);
+ }
+ else
+ {
+ bool isImp = role?.IsImpostor == true;
+ roleText = isImp ? "Impostor" : "Crewmate";
+ hex = isImp ? "FF4D4D" : "00E0FF";
+ }
+ __instance.NameText.text = $"{baseName}\n{roleText}";
+ }
+ else
+ {
+ __instance.NameText.text = baseName;
+ }
+ }
+ }
+}
diff --git a/NewMod/Resources/RoleIcons/crown.png b/NewMod/Resources/RoleIcons/CrownIcon.png
similarity index 100%
rename from NewMod/Resources/RoleIcons/crown.png
rename to NewMod/Resources/RoleIcons/CrownIcon.png
diff --git a/NewMod/Resources/RoleIcons/RadarIcon.png b/NewMod/Resources/RoleIcons/RadarIcon.png
new file mode 100644
index 0000000..fb2c815
Binary files /dev/null and b/NewMod/Resources/RoleIcons/RadarIcon.png differ
diff --git a/NewMod/Resources/RoleIcons/ShieldIcon.png b/NewMod/Resources/RoleIcons/ShieldIcon.png
new file mode 100644
index 0000000..43173de
Binary files /dev/null and b/NewMod/Resources/RoleIcons/ShieldIcon.png differ
diff --git a/NewMod/Resources/RoleIcons/SlashIcon.png b/NewMod/Resources/RoleIcons/SlashIcon.png
new file mode 100644
index 0000000..64485eb
Binary files /dev/null and b/NewMod/Resources/RoleIcons/SlashIcon.png differ
diff --git a/NewMod/Resources/RoleIcons/wraith.png b/NewMod/Resources/RoleIcons/WraithIcon.png
similarity index 100%
rename from NewMod/Resources/RoleIcons/wraith.png
rename to NewMod/Resources/RoleIcons/WraithIcon.png
diff --git a/NewMod/Resources/Shield.png b/NewMod/Resources/Shield.png
new file mode 100644
index 0000000..c7d5dea
Binary files /dev/null and b/NewMod/Resources/Shield.png differ
diff --git a/NewMod/Resources/Slash.png b/NewMod/Resources/Slash.png
new file mode 100644
index 0000000..ec2cfdf
Binary files /dev/null and b/NewMod/Resources/Slash.png differ
diff --git a/NewMod/Resources/newmod-android.bundle b/NewMod/Resources/newmod-android.bundle
index 961e761..68cbb41 100644
Binary files a/NewMod/Resources/newmod-android.bundle and b/NewMod/Resources/newmod-android.bundle differ
diff --git a/NewMod/Resources/newmod-win.bundle b/NewMod/Resources/newmod-win.bundle
index d56ea00..a41b630 100644
Binary files a/NewMod/Resources/newmod-win.bundle and b/NewMod/Resources/newmod-win.bundle differ
diff --git a/NewMod/Roles/CrewmateRoles/Aegis.cs b/NewMod/Roles/CrewmateRoles/Aegis.cs
new file mode 100644
index 0000000..3b7d9d5
--- /dev/null
+++ b/NewMod/Roles/CrewmateRoles/Aegis.cs
@@ -0,0 +1,150 @@
+using System.Text;
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Events;
+using MiraAPI.Events.Mira;
+using MiraAPI.Events.Vanilla.Gameplay;
+using MiraAPI.GameOptions;
+using MiraAPI.Hud;
+using MiraAPI.Roles;
+using NewMod.Components;
+using NewMod.Options.Roles.AegisOptions;
+using NewMod.Utilities;
+using Reactor.Utilities;
+using Reactor.Utilities.Extensions;
+using UnityEngine;
+
+namespace NewMod.Roles.CrewmateRoles
+{
+ public class Aegis : CrewmateRole, INewModRole
+ {
+ public string RoleName => "Aegis";
+ public string RoleDescription => "Project. Protect. Punish.";
+ public string RoleLongDescription => "Deploy a protective zone that reacts to hostile abilities.";
+ public Color RoleColor => new(0.227f, 0.651f, 1f);
+ public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
+ public NewModFaction Faction => NewModFaction.Sentinel;
+ public CustomRoleConfiguration Configuration => new(this)
+ {
+ AffectedByLightOnAirship = true,
+ CanUseSabotage = false,
+ CanUseVent = false,
+ UseVanillaKillButton = false,
+ TasksCountForProgress = true,
+ Icon = NewModAsset.ShieldIcon
+ };
+
+ [HideFromIl2Cpp]
+ public StringBuilder SetTabText()
+ {
+ var tab = INewModRole.GetRoleTabText(this);
+
+ var opts = OptionGroupSingleton.Instance;
+ var mode = opts.Behavior;
+ var cd = opts.AegisCooldown;
+ var dur = opts.DurationSeconds;
+ var radius = opts.Radius;
+ var uses = opts.MaxCharges;
+
+ var cyan = ColorUtility.ToHtmlStringRGB(Color.cyan);
+ var yellow = ColorUtility.ToHtmlStringRGB(Color.yellow);
+ var green = ColorUtility.ToHtmlStringRGB(Palette.AcceptedGreen);
+ var title = ColorUtility.ToHtmlStringRGB(RoleColor);
+
+ tab.AppendLine($"Defensive Support");
+ tab.AppendLine();
+
+ tab.AppendLine($"Mode: {mode}");
+ tab.AppendLine($"Radius: {radius:F1}u • Duration: {dur:F0}s");
+ tab.AppendLine($"Cooldown: {cd:F0}s • Charges: {uses}");
+ tab.AppendLine();
+
+ tab.AppendLine("Tip: Place wards on choke points or common kill paths.");
+
+ return tab;
+ }
+
+ public override bool DidWin(GameOverReason gameOverReason)
+ {
+ return gameOverReason is GameOverReason.CrewmatesByTask or GameOverReason.CrewmatesByVote;
+ }
+
+ [RegisterEvent]
+ public static void OnAnyButtonClick(MiraButtonClickEvent evt)
+ {
+ var mode = ShieldArea.Mode;
+ if (mode == AegisOptions.AegisMode.WarnOnly) return;
+
+ var lp = PlayerControl.LocalPlayer;
+
+ bool block = ShieldArea.IsInsideOthersWard(lp);
+
+ if (!block && evt.Button is CustomActionButton tbtn && tbtn.Target)
+ {
+ var targetPos = tbtn.Target.GetTruePosition();
+ block = ShieldArea.IsInsideOthersWardAt(targetPos, lp.PlayerId);
+ }
+
+ if (!block) return;
+
+ evt.Cancel();
+ NewMod.Instance.Log.LogError("Role Ability Canceled");
+
+ Coroutines.Start(CoroutinesHelper.CoNotify(
+ "Aegis blocks your ability here"));
+ }
+ [RegisterEvent]
+ public static void OnBeforeMurder(BeforeMurderEvent evt)
+ {
+ if (ShieldArea.Mode == AegisOptions.AegisMode.WarnOnly) return;
+ if (MeetingHud.Instance || ExileController.Instance) return;
+ if (!ShieldArea.IsInsideOthersWard(evt.Target)) return;
+
+ evt.Cancel();
+ NewMod.Instance.Log.LogError("Role Ability Canceled Before Murder");
+
+ if (evt.Source.AmOwner)
+ {
+ Coroutines.Start(CoroutinesHelper.CoNotify(
+ "Aegis blocks your kill here"));
+ }
+ foreach (var area in ShieldArea.AreasAt(evt.Target.GetTruePosition()))
+ {
+ var aegis = Utils.PlayerById(area.ownerId);
+ if (aegis.AmOwner)
+ {
+ Coroutines.Start(CoroutinesHelper.CoNotify(
+ $"Aegis Ward Alert: Kill attempt blocked inside your ward!"));
+ }
+ }
+ }
+ [RegisterEvent]
+ public static void OnAfterMurder(AfterMurderEvent evt)
+ {
+ var pos = evt.DeadBody ? (Vector2)evt.DeadBody.transform.position : evt.Target.GetTruePosition();
+
+ foreach (var area in ShieldArea.AreasAt(pos))
+ {
+ var owner = Utils.PlayerById(area.ownerId);
+ if (!owner) continue;
+
+ switch (ShieldArea.Mode)
+ {
+ case AegisOptions.AegisMode.WarnOnly:
+ if (owner.AmOwner)
+ Coroutines.Start(CoroutinesHelper.CoNotify(
+ "A kill happened in your ward"));
+ break;
+
+ case AegisOptions.AegisMode.BlockAndReveal:
+ if (owner.AmOwner)
+ {
+ var killerName = evt.Source.Data.PlayerName;
+ Coroutines.Start(CoroutinesHelper.CoNotify(
+ $"Aegis Ward Alert: A player was killed inside your ward by {killerName}!"));
+ }
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/NewMod/Roles/CrewmateRoles/Beacon.cs b/NewMod/Roles/CrewmateRoles/Beacon.cs
new file mode 100644
index 0000000..15e06ab
--- /dev/null
+++ b/NewMod/Roles/CrewmateRoles/Beacon.cs
@@ -0,0 +1,105 @@
+using System.Text;
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Events;
+using MiraAPI.Events.Vanilla.Gameplay;
+using MiraAPI.Events.Vanilla.Player;
+using MiraAPI.GameOptions;
+using MiraAPI.Roles;
+using MiraAPI.Utilities;
+using NewMod.Options.Roles.BeaconOptions;
+using UnityEngine;
+
+namespace NewMod.Roles.CrewmateRoles
+{
+ public class Beacon : CrewmateRole, INewModRole
+ {
+ public string RoleName => "Beacon";
+ public string RoleDescription => "Scan. Locate. Coordinate.";
+ public string RoleLongDescription => "Send out a map-wide pulse that briefly reveals the position of all players.";
+ public Color RoleColor => new(0.494f, 0.341f, 0.761f);
+ public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
+ public NewModFaction Faction => NewModFaction.Sentinel;
+ public CustomRoleConfiguration Configuration => new(this)
+ {
+ AffectedByLightOnAirship = true,
+ CanUseSabotage = false,
+ CanUseVent = false,
+ UseVanillaKillButton = false,
+ TasksCountForProgress = true,
+ Icon = NewModAsset.RadarIcon
+ };
+
+ [HideFromIl2Cpp]
+ public StringBuilder SetTabText()
+ {
+ var tab = INewModRole.GetRoleTabText(this);
+ var opts = OptionGroupSingleton.Instance;
+
+ var pulseDur = opts.PulseDuration;
+ var cd = opts.PulseCooldown;
+ var taskPerCh = opts.TasksPerCharge;
+ var maxCharges = opts.MaxCharges;
+
+ int completedTasks = GetCompletedTasks();
+ int chargesFromTasks = (int)(completedTasks / taskPerCh);
+
+ tab.AppendLine($"Recon Support");
+ tab.AppendLine();
+ tab.AppendLine($"Charges: {chargesFromTasks} / {maxCharges} (+1 per {taskPerCh} tasks)");
+ tab.AppendLine($"Pulse Duration: {pulseDur:F0}s • Cooldown: {cd:F0}s");
+ tab.AppendLine();
+ tab.AppendLine("Tip: Use pulses after lights or suspected kills to catch rotations.");
+
+ return tab;
+ }
+ public override bool DidWin(GameOverReason gameOverReason)
+ {
+ return gameOverReason is GameOverReason.CrewmatesByVote or GameOverReason.CrewmatesByTask;
+ }
+ public static int charges;
+ public static int grantedFromTasks;
+ public static int lastCompletedTasks;
+ public static float cooldownUntil;
+ public static float pulseUntil;
+
+ [RegisterEvent]
+ public static void OnRoundStart(RoundStartEvent evt)
+ {
+ if (PlayerControl.LocalPlayer.Data.Role is not Beacon) return;
+ }
+ [RegisterEvent]
+ public static void OnTaskComplete(CompleteTaskEvent evt)
+ {
+ if (PlayerControl.LocalPlayer.Data.Role is not Beacon) return;
+ UpdateChargesFromTasks();
+ }
+ public static void UpdateChargesFromTasks()
+ {
+ var settings = OptionGroupSingleton.Instance;
+ int completed = GetCompletedTasks();
+ if (completed == lastCompletedTasks) return;
+
+ lastCompletedTasks = completed;
+ int per = (int)settings.TasksPerCharge;
+ int earned = Mathf.Min(completed / per, (int)settings.MaxCharges);
+ int delta = earned - grantedFromTasks;
+
+ if (delta > 0)
+ {
+ charges = Mathf.Clamp(charges + delta, 0, (int)settings.MaxCharges);
+ grantedFromTasks = earned;
+ Helpers.CreateAndShowNotification(
+ $"+{delta} Beacon {(delta > 1 ? "charges" : "charge")} (tasks)",
+ new Color(0.75f, 0.65f, 1f), spr:NewModAsset.RadarIcon.LoadAsset());
+ }
+ }
+ public static int GetCompletedTasks()
+ {
+ var lp = PlayerControl.LocalPlayer;
+ int done = 0;
+ foreach (var t in lp.myTasks)
+ if (t && t.IsComplete) done++;
+ return done;
+ }
+ }
+}
diff --git a/NewMod/Roles/ImpostorRoles/Edgeveil.cs b/NewMod/Roles/ImpostorRoles/Edgeveil.cs
new file mode 100644
index 0000000..0d41d53
--- /dev/null
+++ b/NewMod/Roles/ImpostorRoles/Edgeveil.cs
@@ -0,0 +1,32 @@
+using System.Text;
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.GameOptions;
+using MiraAPI.Roles;
+using NewMod.Options.Roles.EdgeveilOptions;
+using UnityEngine;
+
+namespace NewMod.Roles.ImpostorRoles
+{
+ public class Edgeveil : ImpostorRole, INewModRole
+ {
+ public string RoleName => "Edgeveil";
+ public string RoleDescription => "Draw. Cleave. Sheathe.";
+ public string RoleLongDescription => "Perform a fast iaijutsu slash in a short cone. Anyone caught in the arc is killed.";
+ public Color RoleColor => new(0.90f, 0.20f, 0.35f);
+ public ModdedRoleTeams Team => ModdedRoleTeams.Impostor;
+ public NewModFaction Faction => NewModFaction.Apex;
+ public CustomRoleConfiguration Configuration => new(this)
+ {
+ AffectedByLightOnAirship = false,
+ CanUseSabotage = false,
+ CanUseVent = false,
+ UseVanillaKillButton = false,
+ TasksCountForProgress = false,
+ Icon = NewModAsset.SlashIcon
+ };
+ public override bool DidWin(GameOverReason gameOverReason)
+ {
+ return gameOverReason is GameOverReason.ImpostorsByKill or GameOverReason.ImpostorsBySabotage;
+ }
+ }
+}
diff --git a/NewMod/Roles/ImpostorRoles/Necromancer.cs b/NewMod/Roles/ImpostorRoles/Necromancer.cs
index b115f59..6ea738d 100644
--- a/NewMod/Roles/ImpostorRoles/Necromancer.cs
+++ b/NewMod/Roles/ImpostorRoles/Necromancer.cs
@@ -11,7 +11,7 @@ public class NecromancerRole : ImpostorRole, ICustomRole
{
public string RoleName => "Necromancer";
public string RoleDescription => "You can revive dead players who weren't killed by you";
- public string RoleLongDescription => "As the Necromancer, you possess a unique and powerful ability: the power to bring one dead player back to life. However,\nyou can only revive someone who wasn't killed by you" + (OptionGroupSingleton.Instance.EnableTeleportation ? "\nPress F3 for Teleportation" : "");
+ public string RoleLongDescription => "As the Necromancer, you possess a unique and powerful ability: the power to bring one dead player back to life. However,\nyou can only revive someone who wasn't killed by you";
public Color RoleColor => Palette.AcceptedGreen.FindAlternateColor();
public ModdedRoleTeams Team => ModdedRoleTeams.Impostor;
public RoleOptionsGroup RoleOptionsGroup { get; } = RoleOptionsGroup.Impostor;
diff --git a/NewMod/Roles/NeutralRoles/Tyrant.cs b/NewMod/Roles/NeutralRoles/Tyrant.cs
index ad99d10..ad5e146 100644
--- a/NewMod/Roles/NeutralRoles/Tyrant.cs
+++ b/NewMod/Roles/NeutralRoles/Tyrant.cs
@@ -1,3 +1,4 @@
+using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -94,7 +95,6 @@ void AppendAbilityLine(int index, string text)
return tabText;
}
public int _kills;
- public static Material _circleMat;
public static byte _championId;
public static bool ApexThroneReady;
public static bool ApexThroneOutcomeSet;
@@ -105,54 +105,6 @@ public enum ThroneOutcome { None, ChampionSideWin }
public byte GetChampion() => _championId;
public void SetChampion(byte playerId) => _championId = playerId;
public static void ClearChampion() => _championId = byte.MaxValue;
- public static Material GetCircleMat()
- {
- if (_circleMat) return _circleMat;
- _circleMat = new(Shader.Find("Sprites/Default"))
- {
- renderQueue = 3000
- };
- return _circleMat;
- }
- public static GameObject CreateCircle(Vector3 pos, float radius, Color color, float duration, int segments = 64)
- {
- var go = new GameObject("Tyrant_Circle");
- go.transform.position = pos;
-
- HudManager.Instance.StartCoroutine(Effects.ScaleIn(go.transform, 0f, 1f, 0.5f));
-
- var mf = go.AddComponent();
- var mr = go.AddComponent();
-
- var mat = new Material(GetCircleMat()) { color = color };
- mr.sharedMaterial = mat;
-
- float visualRadius = radius;
-
- segments = Mathf.Max(12, segments);
- var verts = new Vector3[segments + 1];
- var tris = new int[segments * 3];
-
- verts[0] = Vector3.zero;
- for (int i = 0; i < segments; i++)
- {
- float a = i / (float)segments * Mathf.PI * 2f;
- verts[i + 1] = new Vector3(Mathf.Cos(a) * visualRadius, Mathf.Sin(a) * visualRadius, 0f);
- tris[i * 3 + 0] = 0;
- tris[i * 3 + 1] = i + 1;
- tris[i * 3 + 2] = (i == segments - 1) ? 1 : (i + 2);
- }
-
- var mesh = new Mesh { name = "FearPulseFill" };
- mesh.SetVertices(verts);
- mesh.SetTriangles(tris, 0, true);
- mesh.RecalculateBounds();
- mesh.RecalculateNormals();
- mf.sharedMesh = mesh;
-
- Coroutines.Start(CoroutinesHelper.DespawnCircle(go, duration));
- return go;
- }
[RegisterEvent]
public static void OnAfterMurderEvent(AfterMurderEvent evt)
@@ -199,36 +151,65 @@ public static void OnAfterMurderEvent(AfterMurderEvent evt)
[RegisterEvent]
public static void OnMeetingStart(StartMeetingEvent evt)
{
- if (_championId == byte.MaxValue) return;
- if (PlayerControl.LocalPlayer.PlayerId != _championId) return;
-
- var tyrantPlayer = PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(p => p.Data.Role is Tyrant);
- if (!tyrantPlayer) return;
+ Coroutines.Start(CoShowTyrantForChampion(evt.MeetingHud));
+ }
+ public static IEnumerator CoShowTyrantForChampion(MeetingHud hud)
+ {
+ yield return null;
- foreach (var ps in evt.MeetingHud.playerStates)
+ if (PlayerControl.LocalPlayer.PlayerId == _championId)
{
- if (ps.TargetPlayerId == tyrantPlayer.PlayerId)
+ var tyrantPlayer = PlayerControl.AllPlayerControls
+ .ToArray()
+ .FirstOrDefault(p => p && p.Data != null && p.Data.Role is Tyrant);
+
+ if (tyrantPlayer)
{
- ps.NameText.text += "\nTyrant";
- break;
+ foreach (var ps in hud.playerStates)
+ {
+ if (ps.TargetPlayerId == tyrantPlayer.PlayerId)
+ {
+ ps.NameText.text += "\nTyrant";
+ break;
+ }
+ }
+ }
+ else
+ {
+ NewMod.Instance.Log.LogMessage("No Tyrant in this match skipping...");
}
}
+ NewMod.Instance.Log.LogMessage("NO CRASH");
}
+
[RegisterEvent]
public static void OnHandleVote(HandleVoteEvent evt)
{
var voter = evt.VoteData.Owner;
- foreach (var player in PlayerControl.AllPlayerControls)
+
+ var allPlayers = PlayerControl.AllPlayerControls.ToArray();
+
+ foreach (var player in allPlayers)
{
- if (player.Data.Role is not Tyrant tyrant) continue;
- if (voter.PlayerId != _championId) continue;
+ var role = player.Data?.Role;
+ if (role is not Tyrant tyrant)
+ {
+ continue;
+ }
+
+ if (voter.PlayerId != _championId)
+ {
+ continue;
+ }
- bool betrays = evt.TargetId == tyrant.Player.PlayerId;
+ bool betrays = evt.TargetId == player.PlayerId;
if (betrays)
{
if (evt.VoteData.VotedFor(evt.TargetId))
+ {
evt.VoteData.RemovePlayerVote(evt.TargetId);
+ }
evt.VoteData.VoteForPlayer(evt.VoteData.Owner.PlayerId);
evt.VoteData.SetRemainingVotes(0);
@@ -242,6 +223,7 @@ public static void OnHandleVote(HandleVoteEvent evt)
ApexThroneOutcomeSet = true;
Outcome = ThroneOutcome.ChampionSideWin;
}
+
if (voter.AmOwner)
{
var msg = (Outcome == ThroneOutcome.ChampionSideWin)
@@ -252,6 +234,7 @@ public static void OnHandleVote(HandleVoteEvent evt)
break;
}
}
+
[RegisterEvent]
public static void OnProcessVotes(ProcessVotesEvent evt)
{
@@ -285,7 +268,7 @@ public void SpawnSuppressionDome(Vector3 pos)
area.Init(Player.PlayerId, radius: OptionGroupSingleton.Instance.DomeRadius, OptionGroupSingleton.Instance.DomeDuration);
if (Player.AmOwner)
- CreateCircle(Player.GetTruePosition(), OptionGroupSingleton.Instance.DomeRadius, Palette.AcceptedGreen, OptionGroupSingleton.Instance.DomeDuration);
+ Utils.CreateCircle("SupressionDome", Player.GetTruePosition(), OptionGroupSingleton.Instance.DomeRadius, Palette.AcceptedGreen, OptionGroupSingleton.Instance.DomeDuration);
}
public void ArmWitnessTrap(Vector3 pos)
{
@@ -301,7 +284,7 @@ public void ArmWitnessTrap(Vector3 pos)
);
if (Player.AmOwner)
- CreateCircle(Player.GetTruePosition(), OptionGroupSingleton.Instance.WitnessRange, Color.cyan, OptionGroupSingleton.Instance.WitnessArmWindow);
+ Utils.CreateCircle("ArmWitnessTrap", Player.GetTruePosition(), OptionGroupSingleton.Instance.WitnessRange, Color.cyan, OptionGroupSingleton.Instance.WitnessArmWindow);
}
public void SpawnFearPulse(Vector3 pos)
{
@@ -317,7 +300,7 @@ public void SpawnFearPulse(Vector3 pos)
);
if (Player.AmOwner)
- CreateCircle(Player.GetTruePosition(), OptionGroupSingleton.Instance.FearPulseRadius, new Color(1f, 0.35f, 0.2f, 0.6f), OptionGroupSingleton.Instance.FearPulseDuration);
+ Utils.CreateCircle("FearPulse", Player.GetTruePosition(), OptionGroupSingleton.Instance.FearPulseRadius, new Color(1f, 0.35f, 0.2f, 0.6f), OptionGroupSingleton.Instance.FearPulseDuration);
}
[MethodRpc((uint)CustomRPC.NotifyChampion)]
public static void RpcNotifyChampion(PlayerControl source, PlayerControl target)
diff --git a/NewMod/Roles/NeutralRoles/WraithCaller.cs b/NewMod/Roles/NeutralRoles/WraithCaller.cs
index f51db32..da6ea51 100644
--- a/NewMod/Roles/NeutralRoles/WraithCaller.cs
+++ b/NewMod/Roles/NeutralRoles/WraithCaller.cs
@@ -1,7 +1,6 @@
using System.Text;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameOptions;
-using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using NewMod.Options.Roles.WraithCallerOptions;
using NewMod.Utilities;
@@ -9,7 +8,6 @@
namespace NewMod.Roles.NeutralRoles
{
- [MiraIgnore]
public class WraithCaller : ImpostorRole, INewModRole
{
public string RoleName => "Wraith Caller";
diff --git a/NewMod/Utilities/AegisUtilities.cs b/NewMod/Utilities/AegisUtilities.cs
new file mode 100644
index 0000000..054ef8d
--- /dev/null
+++ b/NewMod/Utilities/AegisUtilities.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using UnityEngine;
+using NewMod.Components;
+using NewMod.Options.Roles.AegisOptions;
+using MiraAPI.GameOptions;
+using Reactor.Utilities.Extensions;
+using Reactor.Utilities;
+
+namespace NewMod.Utilities
+{
+ public static class AegisUtilities
+ {
+ public static readonly HashSet ActiveOwners = new();
+ public static bool HasActiveShield()
+ {
+ var lp = PlayerControl.LocalPlayer;
+ return lp && ActiveOwners.Contains(lp.PlayerId);
+ }
+ public static void ActivateShield(PlayerControl owner, Vector2 position)
+ {
+ if (!owner) return;
+
+ var opts = OptionGroupSingleton.Instance;
+
+ var go = new GameObject("AegisShieldArea").DontDestroy();
+ go.transform.position = position;
+
+ var area = go.AddComponent();
+ area.Init(owner.PlayerId, opts.Radius, opts.DurationSeconds);
+
+ ActiveOwners.Add(owner.PlayerId);
+ Coroutines.Start(CoCleanupOwner(owner.PlayerId, opts.DurationSeconds));
+ }
+
+ static System.Collections.IEnumerator CoCleanupOwner(byte ownerId, float duration)
+ {
+ yield return new WaitForSeconds(duration);
+ ActiveOwners.Remove(ownerId);
+ }
+ }
+}
diff --git a/NewMod/Utilities/CoroutinesHelper.cs b/NewMod/Utilities/CoroutinesHelper.cs
index a894ed8..3a48032 100644
--- a/NewMod/Utilities/CoroutinesHelper.cs
+++ b/NewMod/Utilities/CoroutinesHelper.cs
@@ -8,6 +8,7 @@
using MiraAPI.Networking;
using NewMod.Roles.NeutralRoles;
using Reactor.Utilities.Extensions;
+using NewMod.Components.ScreenEffects;
namespace NewMod.Utilities
{
@@ -421,10 +422,35 @@ public static IEnumerator ResetRepelEffect(PlayerControl target, float delay)
target.MyPhysics.body.velocity = Vector2.zero;
}
}
+ ///
+ /// Coroutine that waits for a given duration before destroying a specified GameObject.
+ ///
+ /// The GameObject to destroy after the delay.
+ /// The time in seconds to wait before destroying the object.
+ /// IEnumerator for coroutine execution.
public static IEnumerator DespawnCircle(GameObject go, float duration)
{
yield return new WaitForSeconds(duration);
go.Destroy();
}
+
+ ///
+ /// Coroutine that waits for a given duration and then removes
+ /// specific visual effects (Earthquake, Glitch, SlowPulseHue) from a Camera.
+ ///
+ /// The Camera to check for and remove effects from.
+ /// The time in seconds to wait before removing the effects.
+ /// IEnumerator for coroutine execution.
+ public static IEnumerator RemoveCameraEffect(Camera cam, float duration)
+ {
+ yield return new WaitForSeconds(duration);
+
+ if (cam.TryGetComponent(out var eq))
+ Object.Destroy(eq);
+ if (cam.TryGetComponent(out var ge))
+ Object.Destroy(ge);
+ if (cam.TryGetComponent(out var hue))
+ Object.Destroy(hue);
+ }
}
}
diff --git a/NewMod/Utilities/Utils.cs b/NewMod/Utilities/Utils.cs
index eca5f22..69d74d2 100644
--- a/NewMod/Utilities/Utils.cs
+++ b/NewMod/Utilities/Utils.cs
@@ -23,6 +23,8 @@
using MiraAPI.Hud;
using NewMod.Buttons.Pulseblade;
using NewMod.Roles;
+using NewMod.Components;
+using NewMod.Buttons.WraithCaller;
namespace NewMod.Utilities
{
@@ -76,6 +78,8 @@ public static class Utils
///
public static readonly Dictionary StrikeKills = new();
+ public static Material _circleMat;
+
///
/// Retrieves a PlayerControl instance by its player ID.
///
@@ -730,7 +734,6 @@ public static void RpcMissionFails(PlayerControl source, PlayerControl target)
}
}
}
-
}
public static string GetFactionDisplay(INewModRole role)
{
@@ -738,6 +741,7 @@ public static string GetFactionDisplay(INewModRole role)
{
NewModFaction.Apex => $"Apex",
NewModFaction.Entropy => $"Entropy",
+ NewModFaction.Sentinel => $"Sentinel",
_ => $"Unknown"
};
}
@@ -920,6 +924,7 @@ public static IEnumerator FadeAndDestroy(GameObject ghost, float fadeDuration)
{ typeof(SpecialAgent), new() { typeof(AssignButton) } },
{ typeof(TheVisionary), new() { typeof(CaptureButton), typeof(ShowScreenshotButton) } },
{ typeof(PulseBlade), new() { typeof(StrikeButton)}},
+ { typeof(WraithCaller), new() {typeof(CallWraithButton) } }
// TODO: Add Launchpad roles and their associated buttons here
};
@@ -1071,10 +1076,10 @@ public static IEnumerator CoShakeCamera(FollowerCamera cam, float duration)
}
///
- /// Formats a into a string with the format:
+ /// Formats a into a string with the format:
/// dd:hh:mm:ss.
///
- /// The to format.
+ /// The to format.
public static string FormatSpan(System.TimeSpan t)
{
int dd = Mathf.Max(0, t.Days);
@@ -1083,5 +1088,91 @@ public static string FormatSpan(System.TimeSpan t)
int ss = Mathf.Clamp(t.Seconds, 0, 59);
return $"{dd:D1}:{hh:D2}:{mm:D2}:{ss:D2}";
}
+ ///
+ /// Finds the surveillance console on the current ship.
+ ///
+ ///
+ /// The first instance representing the surveillance console,
+ ///
+ public static SystemConsole FindSurveillanceConsole()
+ {
+ var all = ShipStatus.Instance?.AllConsoles;
+ var sys = all.OfType().FirstOrDefault(c => c && c.MinigamePrefab && c.MinigamePrefab is SurveillanceMinigame);
+
+ return all.OfType().FirstOrDefault(c =>
+ {
+ var n = c.name;
+ return n.Contains("Surv", System.StringComparison.OrdinalIgnoreCase)
+ || n.Contains("Lookout", System.StringComparison.OrdinalIgnoreCase);
+ });
+ }
+
+ ///
+ /// Retrieves or creates a material used for drawing circles.
+ ///
+ ///
+ /// A instance with the "Sprites/Default" shader
+ ///
+ public static Material GetCircleMat()
+ {
+ if (_circleMat) return _circleMat;
+ _circleMat = new(Shader.Find("Sprites/Default"))
+ {
+ renderQueue = 3000
+ };
+ return _circleMat;
+ }
+
+ ///
+ /// Creates a filled circle mesh in the scene at a given position.
+ ///
+ /// The name of the created GameObject.
+ /// The position where the circle will be created.
+ /// The radius of the circle.
+ /// The color to apply to the circle material.
+ /// How long the circle should remain before being despawned.
+ /// Number of segments for the circle geometry. Minimum of 12.
+ ///
+ /// The created representing the circle.
+ ///
+ public static GameObject CreateCircle(string name, Vector3 pos, float radius, Color color, float duration, int segments = 64)
+ {
+ var go = new GameObject(name);
+ go.transform.position = pos;
+
+ HudManager.Instance.StartCoroutine(Effects.ScaleIn(go.transform, 0f, 1f, 0.5f));
+
+ var mf = go.AddComponent();
+ var mr = go.AddComponent();
+
+ var mat = new Material(GetCircleMat()) { color = color };
+ mr.sharedMaterial = mat;
+
+ float visualRadius = radius;
+
+ segments = Mathf.Max(12, segments);
+ var verts = new Vector3[segments + 1];
+ var tris = new int[segments * 3];
+
+ verts[0] = Vector3.zero;
+ for (int i = 0; i < segments; i++)
+ {
+ float a = i / (float)segments * Mathf.PI * 2f;
+ verts[i + 1] = new Vector3(Mathf.Cos(a) * visualRadius, Mathf.Sin(a) * visualRadius, 0f);
+ tris[i * 3 + 0] = 0;
+ tris[i * 3 + 1] = i + 1;
+ tris[i * 3 + 2] = (i == segments - 1) ? 1 : (i + 2);
+ }
+
+ var mesh = new Mesh { name = $"{name}_Fill" };
+ mesh.SetVertices(verts);
+ mesh.SetTriangles(tris, 0, true);
+ mesh.RecalculateBounds();
+ mesh.RecalculateNormals();
+ mf.sharedMesh = mesh;
+
+ Coroutines.Start(CoroutinesHelper.DespawnCircle(go, duration));
+ return go;
+ }
}
}
diff --git a/libs/MiraAPI.dll b/libs/MiraAPI.dll
index 3045efe..1a553d5 100644
Binary files a/libs/MiraAPI.dll and b/libs/MiraAPI.dll differ