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