diff --git a/CTA/CustomTeamAssigner/CustomRoles.cs b/CTA/CustomTeamAssigner/CustomRoles.cs
index a02489738..44f24c628 100644
--- a/CTA/CustomTeamAssigner/CustomRoles.cs
+++ b/CTA/CustomTeamAssigner/CustomRoles.cs
@@ -374,6 +374,9 @@ public enum CustomRoles
// Capture The Flag
CTFPlayer,
+
+ // Natural Disasters
+ NDPlayer,
// GM
GM,
diff --git a/CustomRoles.cs b/CustomRoles.cs
index a02489738..1951b8db6 100644
--- a/CustomRoles.cs
+++ b/CustomRoles.cs
@@ -375,6 +375,9 @@ public enum CustomRoles
// Capture The Flag
CTFPlayer,
+ // Natural Disasters
+ NDPlayer,
+
// GM
GM,
diff --git a/EHR.sln.DotSettings b/EHR.sln.DotSettings
index 5f4a04ab0..e4ecf135f 100644
--- a/EHR.sln.DotSettings
+++ b/EHR.sln.DotSettings
@@ -23,6 +23,7 @@
LI
NB
NC
+ ND
NE
NK
NNK
diff --git a/GameMode/NaturalDisasters.cs b/GameMode/NaturalDisasters.cs
new file mode 100644
index 000000000..07b8d0e7c
--- /dev/null
+++ b/GameMode/NaturalDisasters.cs
@@ -0,0 +1,763 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using AmongUs.GameOptions;
+using HarmonyLib;
+using UnityEngine;
+using Random = UnityEngine.Random;
+
+/*
+ * Earthquake: <#000000>█<#000000>█<#5e5e5e>█<#adadad>█████
<#5e5e5e>█<#000000>█<#5e5e5e>█<#adadad>█<#adadad>████
<#5e5e5e>█<#000000>█<#000000>█<#5e5e5e>█<#5e5e5e>█<#adadad>█<#adadad>██
<#adadad>█<#5e5e5e>█<#000000>█<#000000>█<#000000>█<#5e5e5e>█<#5e5e5e>█<#adadad>█
█<#adadad>█<#5e5e5e>█<#5e5e5e>█<#000000>█<#000000>█<#000000>█<#5e5e5e>█
██<#adadad>█<#adadad>█<#5e5e5e>█<#5e5e5e>█<#000000>█<#000000>█
████<#adadad>█<#adadad>█<#5e5e5e>█<#000000>█
█████<#adadad>█<#5e5e5e>█<#000000>█
+ * Meteor: ███<#fff700>█<#fff700>████
██<#fff700>█<#ffae00>█<#ffae00>█<#fff700>███
█<#fff700>█<#ffae00>█<#ff6f00>█<#ff6f00>█<#ffae00>█<#fff700>██
<#fff700>█<#ffae00>█<#ff6f00>█<#ff1100>█<#ff1100>█<#ff6f00>█<#ffae00>█<#fff700>█
<#fff700>█<#ffae00>█<#ff6f00>█<#ff1100>█<#ff1100>█<#ff6f00>█<#ffae00>█<#fff700>█
█<#fff700>█<#ffae00>█<#ff6f00>█<#ff6f00>█<#ffae00>█<#fff700>██
██<#fff700>█<#ffae00>█<#ffae00>█<#fff700>███
███<#fff700>█<#fff700>████
+ * Volcano Eruption:
+ * 1. ██████
██████
██<#ff6200>█<#ff6200>███
██<#ff6200>█<#ff6200>███
██████
██████
+ * 2. ██████
█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>██
█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>██
█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>██
█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>██
██████
+ * 3. <#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
+ * 4. <#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█<#ff6200>█
+ * Tornado: ██<#dbdbdb>█<#dbdbdb>█<#dbdbdb>█<#dbdbdb>███
█<#dbdbdb>█<#b0b0b0>█<#b0b0b0>█<#b0b0b0>█<#b0b0b0>█<#dbdbdb>██
<#dbdbdb>█<#b0b0b0>█<#b0b0b0>█<#828282>█<#828282>█<#b0b0b0>█<#b0b0b0>█<#dbdbdb>█
<#dbdbdb>█<#b0b0b0>█<#828282>█<#474747>█<#474747>█<#828282>█<#b0b0b0>█<#dbdbdb>█
<#dbdbdb>█<#b0b0b0>█<#828282>█<#474747>█<#474747>█<#828282>█<#b0b0b0>█<#dbdbdb>█
<#dbdbdb>█<#b0b0b0>█<#b0b0b0>█<#828282>█<#828282>█<#b0b0b0>█<#b0b0b0>█<#dbdbdb>█
█<#dbdbdb>█<#b0b0b0>█<#b0b0b0>█<#b0b0b0>█<#b0b0b0>█<#dbdbdb>██
██<#dbdbdb>█<#dbdbdb>█<#dbdbdb>█<#dbdbdb>███
+ * Thunderstorm strike: █████<#c6c7c3>█
█<#c6c7c3>███<#c6c7c3>██
<#c6c7c3>██<#fffb00>█<#fffb00>███
██<#fffb00>█<#fffb00>██<#c6c7c3>█
<#c6c7c3>██████
██<#c6c7c3>█<#c6c7c3>███
+ * Sandstorm: █<#cf935f>█<#ffd3c2>███<#cf935f>█<#ffd3c2>██
█<#ffd3c2>██<#cf935f>█<#ffd3c2>███<#cf935f>█
<#cf935f>██<#ffd3c2>█<#ffd3c2>██<#cf935f>███
<#ffd3c2>█<#ffd3c2>█<#cf935f>█<#ffd3c2>███<#ffd3c2>█<#cf935f>█
██<#ffd3c2>██<#cf935f>█<#ffd3c2>██<#ffd3c2>█
<#ffd3c2>█<#cf935f>██<#ffd3c2>██<#ffd3c2>█<#cf935f>██
██<#ffd3c2>█<#cf935f>████<#ffd3c2>█
<#cf935f>█<#ffd3c2>███<#ffd3c2>█<#cf935f>██<#cf935f>█
+ * Tsunami:
+ * Left to right: <#0073ff>█<#0095ff>█<#00ccff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█
<#0073ff>█<#0095ff>█<#00ccff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█
<#0073ff>█<#0095ff>█<#00ccff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█
<#0073ff>█<#0095ff>█<#00ccff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█
<#0073ff>█<#0095ff>█<#00ccff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█
<#0073ff>█<#0095ff>█<#00ccff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█
<#0073ff>█<#0095ff>█<#00ccff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█
<#0073ff>█<#0095ff>█<#00ccff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█
+ * Right to left: <#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#00ccff>█<#0095ff>█<#0073ff>█
<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#00ccff>█<#0095ff>█<#0073ff>█
<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#00ccff>█<#0095ff>█<#0073ff>█
<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#00ccff>█<#0095ff>█<#0073ff>█
<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#00ccff>█<#0095ff>█<#0073ff>█
<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#00ccff>█<#0095ff>█<#0073ff>█
<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#00ccff>█<#0095ff>█<#0073ff>█
<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#9ce8ff>█<#00ccff>█<#0095ff>█<#0073ff>█
+ * Top to bottom: <#003cff>█<#003cff>█<#003cff>█<#003cff>█<#003cff>█<#003cff>█<#003cff>█<#003cff>█
<#006aff>█<#006aff>█<#006aff>█<#006aff>█<#006aff>█<#006aff>█<#006aff>█<#006aff>█
<#00bbff>█<#00bbff>█<#00bbff>█<#00bbff>█<#00bbff>█<#00bbff>█<#00bbff>█<#00bbff>█
<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█
<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█
<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█
<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█
<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█<#b8e1ff>█
+ * Bottom to top: <#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█
<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█
<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█
<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█
<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█<#c2e2ff>█
<#00bfff>█<#00bfff>█<#00bfff>█<#00bfff>█<#00bfff>█<#00bfff>█<#00bfff>█<#00bfff>█
<#007bff>█<#007bff>█<#007bff>█<#007bff>█<#007bff>█<#007bff>█<#007bff>█<#007bff>█
<#0033ff>█<#0033ff>█<#0033ff>█<#0033ff>█<#0033ff>█<#0033ff>█<#0033ff>█<#0033ff>█
+ * Sinkhole: <#7d7d7d>█<#7d7d7d>█<#545454>█<#7d7d7d>█<#7d7d7d>█<#7d7d7d>█<#7d7d7d>█<#545454>█
<#545454>█<#424242>█<#424242>█<#424242>█<#424242>█<#424242>█<#424242>█<#7d7d7d>█
<#7d7d7d>█<#424242>█<#000000>█<#000000>█<#000000>█<#000000>█<#424242>█<#7d7d7d>█
<#545454>█<#424242>█<#000000>█<#000000>█<#000000>█<#000000>█<#424242>█<#7d7d7d>█
<#7d7d7d>█<#424242>█<#000000>█<#000000>█<#000000>█<#000000>█<#424242>█<#545454>█
<#545454>█<#424242>█<#000000>█<#000000>█<#000000>█<#000000>█<#424242>█<#7d7d7d>█
<#7d7d7d>█<#424242>█<#424242>█<#424242>█<#424242>█<#424242>█<#424242>█<#7d7d7d>█
<#545454>█<#545454>█<#7d7d7d>█<#7d7d7d>█<#545454>█<#7d7d7d>█<#7d7d7d>█<#545454>█
+ */
+
+namespace EHR
+{
+ public static class NaturalDisasters
+ {
+ private const float Range = 1.5f;
+ private static List AllDisasters = [];
+ private static readonly List ActiveDisasters = [];
+ private static readonly List PreparingDisasters = [];
+
+ private static ((float Left, float Right) X, (float Top, float Bottom) Y) MapBounds;
+
+ private static OptionItem DisasterFrequency;
+ private static OptionItem DisasterWarningTime;
+
+ public static void SetupCustomOption()
+ {
+ const int id = 69_216_001;
+ Color color = Utils.GetRoleColor(CustomRoles.NDPlayer);
+
+ DisasterFrequency = new IntegerOptionItem(id, "ND_DisasterFrequency", new(1, 20, 1), 2, TabGroup.GameSettings)
+ .SetHeader(true)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Seconds);
+
+ DisasterWarningTime = new IntegerOptionItem(id + 1, "ND_DisasterWarningTime", new(1, 30, 1), 5, TabGroup.GameSettings)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Seconds);
+
+ AllDisasters = Assembly.GetExecutingAssembly()
+ .GetTypes()
+ .Where(x => x.IsClass && !x.IsAbstract && x.IsSubclassOf(typeof(Disaster)))
+ .ToList();
+
+ AllDisasters.ForEach(x => x.GetMethod("SetupOwnCustomOption")?.Invoke(null, null));
+ }
+
+ public static void OnGameStart()
+ {
+ ActiveDisasters.Clear();
+ PreparingDisasters.Clear();
+ Sinkhole.Sinkholes.Clear();
+ BuildingCollapse.CollapsedRooms.Clear();
+ BuildingCollapse.LastPosition.Clear();
+
+ if (Options.CurrentGameMode != CustomGameMode.NaturalDisasters) return;
+
+ var rooms = Main.CurrentMap switch
+ {
+ MapNames.Skeld => new RandomSpawn.SkeldSpawnMap().positions.Values,
+ MapNames.Mira => new RandomSpawn.MiraHQSpawnMap().positions.Values,
+ MapNames.Polus => new RandomSpawn.PolusSpawnMap().positions.Values,
+ MapNames.Dleks => new RandomSpawn.DleksSpawnMap().positions.Values,
+ MapNames.Airship => new RandomSpawn.AirshipSpawnMap().positions.Values,
+ MapNames.Fungle => new RandomSpawn.FungleSpawnMap().positions.Values,
+ _ => null
+ };
+ if (rooms == null) return;
+
+ var x = rooms.Select(r => r.x).ToArray();
+ var y = rooms.Select(r => r.y).ToArray();
+
+ const float extend = 2.5f;
+ MapBounds = ((x.Min() - extend, x.Max() + extend), (y.Min() - extend, y.Max() + extend));
+ }
+
+ public static void ApplyGameOptions(IGameOptions opt, byte id)
+ {
+ ActiveDisasters.ForEach(x => x.ApplyOwnGameOptions(opt, id));
+ }
+
+ public static string GetSuffixText() // TODO: implement this
+ {
+ return string.Empty;
+ }
+
+ public static int GetSurvivalTime(byte id) // TODO: implement this
+ {
+ return 0;
+ }
+
+ public static Color DeathReasonColor(PlayerState.DeathReason deathReason) => deathReason switch
+ {
+ PlayerState.DeathReason.Meteor => Color.red,
+ PlayerState.DeathReason.Lava => Palette.Orange,
+ PlayerState.DeathReason.Tornado => Color.gray,
+ PlayerState.DeathReason.Lightning => Palette.White_75Alpha,
+ PlayerState.DeathReason.Drowned => Color.cyan,
+ PlayerState.DeathReason.Sunken => Color.yellow,
+ PlayerState.DeathReason.Collapsed => Palette.Brown,
+ _ => Color.white
+ };
+
+ private static string Sprite(string name) => name switch
+ {
+ "Earthquake" => "<#000000>\u2588<#000000>\u2588<#5e5e5e>\u2588<#adadad>\u2588\u2588\u2588\u2588\u2588
<#5e5e5e>\u2588<#000000>\u2588<#5e5e5e>\u2588<#adadad>\u2588<#adadad>\u2588\u2588\u2588\u2588
<#5e5e5e>\u2588<#000000>\u2588<#000000>\u2588<#5e5e5e>\u2588<#5e5e5e>\u2588<#adadad>\u2588<#adadad>\u2588\u2588
<#adadad>\u2588<#5e5e5e>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#5e5e5e>\u2588<#5e5e5e>\u2588<#adadad>\u2588
\u2588<#adadad>\u2588<#5e5e5e>\u2588<#5e5e5e>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#5e5e5e>\u2588
\u2588\u2588<#adadad>\u2588<#adadad>\u2588<#5e5e5e>\u2588<#5e5e5e>\u2588<#000000>\u2588<#000000>\u2588
\u2588\u2588\u2588\u2588<#adadad>\u2588<#adadad>\u2588<#5e5e5e>\u2588<#000000>\u2588
\u2588\u2588\u2588\u2588\u2588<#adadad>\u2588<#5e5e5e>\u2588<#000000>\u2588
",
+ "Meteor" => "\u2588\u2588\u2588<#fff700>\u2588<#fff700>\u2588\u2588\u2588\u2588
\u2588\u2588<#fff700>\u2588<#ffae00>\u2588<#ffae00>\u2588<#fff700>\u2588\u2588\u2588
\u2588<#fff700>\u2588<#ffae00>\u2588<#ff6f00>\u2588<#ff6f00>\u2588<#ffae00>\u2588<#fff700>\u2588\u2588
<#fff700>\u2588<#ffae00>\u2588<#ff6f00>\u2588<#ff1100>\u2588<#ff1100>\u2588<#ff6f00>\u2588<#ffae00>\u2588<#fff700>\u2588
<#fff700>\u2588<#ffae00>\u2588<#ff6f00>\u2588<#ff1100>\u2588<#ff1100>\u2588<#ff6f00>\u2588<#ffae00>\u2588<#fff700>\u2588
\u2588<#fff700>\u2588<#ffae00>\u2588<#ff6f00>\u2588<#ff6f00>\u2588<#ffae00>\u2588<#fff700>\u2588\u2588
\u2588\u2588<#fff700>\u2588<#ffae00>\u2588<#ffae00>\u2588<#fff700>\u2588\u2588\u2588
\u2588\u2588\u2588<#fff700>\u2588<#fff700>\u2588\u2588\u2588\u2588
",
+ "VolcanoEruption" => "\u2588\u2588\u2588\u2588\u2588\u2588
\u2588\u2588\u2588\u2588\u2588\u2588
\u2588\u2588<#ff6200>\u2588<#ff6200>\u2588\u2588\u2588
\u2588\u2588<#ff6200>\u2588<#ff6200>\u2588\u2588\u2588
\u2588\u2588\u2588\u2588\u2588\u2588
\u2588\u2588\u2588\u2588\u2588\u2588
",
+ "Tornado" => "\u2588\u2588<#dbdbdb>\u2588<#dbdbdb>\u2588<#dbdbdb>\u2588<#dbdbdb>\u2588\u2588\u2588
\u2588<#dbdbdb>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#dbdbdb>\u2588\u2588
<#dbdbdb>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#828282>\u2588<#828282>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#dbdbdb>\u2588
<#dbdbdb>\u2588<#b0b0b0>\u2588<#828282>\u2588<#474747>\u2588<#474747>\u2588<#828282>\u2588<#b0b0b0>\u2588<#dbdbdb>\u2588
<#dbdbdb>\u2588<#b0b0b0>\u2588<#828282>\u2588<#474747>\u2588<#474747>\u2588<#828282>\u2588<#b0b0b0>\u2588<#dbdbdb>\u2588
<#dbdbdb>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#828282>\u2588<#828282>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#dbdbdb>\u2588
\u2588<#dbdbdb>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#b0b0b0>\u2588<#dbdbdb>\u2588\u2588
\u2588\u2588<#dbdbdb>\u2588<#dbdbdb>\u2588<#dbdbdb>\u2588<#dbdbdb>\u2588\u2588\u2588
",
+ "Sandstorm" => "\u2588<#cf935f>\u2588<#ffd3c2>\u2588\u2588\u2588<#cf935f>\u2588<#ffd3c2>\u2588\u2588
\u2588<#ffd3c2>\u2588\u2588<#cf935f>\u2588<#ffd3c2>\u2588\u2588\u2588<#cf935f>\u2588
<#cf935f>\u2588\u2588<#ffd3c2>\u2588<#ffd3c2>\u2588\u2588<#cf935f>\u2588\u2588\u2588
<#ffd3c2>\u2588<#ffd3c2>\u2588<#cf935f>\u2588<#ffd3c2>\u2588\u2588\u2588<#ffd3c2>\u2588<#cf935f>\u2588
\u2588\u2588<#ffd3c2>\u2588\u2588<#cf935f>\u2588<#ffd3c2>\u2588\u2588<#ffd3c2>\u2588
<#ffd3c2>\u2588<#cf935f>\u2588\u2588<#ffd3c2>\u2588\u2588<#ffd3c2>\u2588<#cf935f>\u2588\u2588
\u2588\u2588<#ffd3c2>\u2588<#cf935f>\u2588\u2588\u2588\u2588<#ffd3c2>\u2588
<#cf935f>\u2588<#ffd3c2>\u2588\u2588\u2588<#ffd3c2>\u2588<#cf935f>\u2588\u2588<#cf935f>\u2588
",
+ "Tsunami" => Tsunami.Sprites[Enum.GetValues().RandomElement()],
+ "Sinkhole" => "<#7d7d7d>\u2588<#7d7d7d>\u2588<#545454>\u2588<#7d7d7d>\u2588<#7d7d7d>\u2588<#7d7d7d>\u2588<#7d7d7d>\u2588<#545454>\u2588
<#545454>\u2588<#424242>\u2588<#424242>\u2588<#424242>\u2588<#424242>\u2588<#424242>\u2588<#424242>\u2588<#7d7d7d>\u2588
<#7d7d7d>\u2588<#424242>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#424242>\u2588<#7d7d7d>\u2588
<#545454>\u2588<#424242>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#424242>\u2588<#7d7d7d>\u2588
<#7d7d7d>\u2588<#424242>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#424242>\u2588<#545454>\u2588
<#545454>\u2588<#424242>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#000000>\u2588<#424242>\u2588<#7d7d7d>\u2588
<#7d7d7d>\u2588<#424242>\u2588<#424242>\u2588<#424242>\u2588<#424242>\u2588<#424242>\u2588<#424242>\u2588<#7d7d7d>\u2588
<#545454>\u2588<#545454>\u2588<#7d7d7d>\u2588<#7d7d7d>\u2588<#545454>\u2588<#7d7d7d>\u2588<#7d7d7d>\u2588<#545454>\u2588
",
+ _ => string.Empty
+ };
+
+ [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))]
+ static class FixedUpdatePatch
+ {
+ private static long LastDisaster = Utils.TimeStamp;
+
+ public static void Postfix(PlayerControl __instance)
+ {
+ if (!AmongUsClient.Instance.AmHost || !GameStates.IsInTask || Options.CurrentGameMode != CustomGameMode.NaturalDisasters || Main.HasJustStarted || !__instance.IsHost()) return;
+
+ foreach (NaturalDisaster naturalDisaster in PreparingDisasters.ToArray())
+ {
+ naturalDisaster.Update();
+ if (float.IsNaN(naturalDisaster.SpawnTimer))
+ {
+ var type = AllDisasters.Find(d => d.Name == naturalDisaster.DisasterName);
+ Activator.CreateInstance(type, naturalDisaster.Position, naturalDisaster);
+ PreparingDisasters.Remove(naturalDisaster);
+ }
+ }
+
+ ActiveDisasters.ForEach(x => x.Update());
+ Sinkhole.OnFixedUpdate();
+ BuildingCollapse.OnFixedUpdate();
+
+ if (Utils.TimeStamp - LastDisaster >= DisasterFrequency.GetInt())
+ {
+ LastDisaster = Utils.TimeStamp;
+ var disaster = AllDisasters.RandomElement();
+ var position = new Vector2(Random.Range(MapBounds.X.Left, MapBounds.X.Right), Random.Range(MapBounds.Y.Top, MapBounds.Y.Bottom));
+ PreparingDisasters.Add(new(position, DisasterWarningTime.GetFloat(), Sprite(disaster.Name), disaster.Name));
+ }
+ }
+ }
+
+ abstract class Disaster
+ {
+ protected Disaster(Vector2 position)
+ {
+ Position = position;
+ ActiveDisasters.Add(this);
+ }
+
+ private long StartTimeStamp { get; } = Utils.TimeStamp;
+ protected Vector2 Position { get; set; }
+ protected NaturalDisaster NetObject { get; init; } = null;
+ protected virtual int Duration { get; set; } = 0;
+
+ protected virtual bool RemoveIfExpired()
+ {
+ if (Duration == 0 || Utils.TimeStamp - StartTimeStamp >= Duration)
+ {
+ ActiveDisasters.Remove(this);
+ NetObject.Despawn();
+ return true;
+ }
+
+ return false;
+ }
+
+ public abstract void Update();
+
+ public virtual void ApplyOwnGameOptions(IGameOptions opt, byte id)
+ {
+ }
+ }
+
+ sealed class Earthquake : Disaster
+ {
+ private static OptionItem DurationOpt;
+ private static OptionItem Speed;
+
+ public Earthquake(Vector2 position, NaturalDisaster naturalDisaster) : base(position)
+ {
+ NetObject = naturalDisaster;
+ Update();
+ }
+
+ protected override int Duration { get; set; } = DurationOpt.GetInt();
+
+ public static void SetupOwnCustomOption()
+ {
+ const int id = 69_216_100;
+ Color color = Utils.GetRoleColor(CustomRoles.NDPlayer);
+
+ DurationOpt = new IntegerOptionItem(id, "ND_Earthquake.DurationOpt", new(1, 120, 1), 30, TabGroup.GameSettings)
+ .SetHeader(true)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Seconds);
+
+ Speed = new FloatOptionItem(id + 1, "ND_Earthquake.Speed", new(0.05f, 2f, 0.05f), 0.2f, TabGroup.GameSettings)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Multiplier);
+ }
+
+ public override void Update()
+ {
+ if (RemoveIfExpired()) return;
+
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ if (Vector2.Distance(pc.Pos(), this.Position) <= Range)
+ {
+ float speed = Speed.GetFloat();
+ if (Mathf.Approximately(Main.AllPlayerSpeed[pc.PlayerId], speed)) continue;
+ Main.AllPlayerSpeed[pc.PlayerId] = speed;
+ pc.MarkDirtySettings();
+ }
+ else
+ {
+ float speed = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod);
+ if (Mathf.Approximately(Main.AllPlayerSpeed[pc.PlayerId], speed)) continue;
+ Main.AllPlayerSpeed[pc.PlayerId] = speed;
+ pc.MarkDirtySettings();
+ }
+ }
+ }
+
+ protected override bool RemoveIfExpired()
+ {
+ if (base.RemoveIfExpired())
+ {
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ float speed = Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod);
+ if (Mathf.Approximately(Main.AllPlayerSpeed[pc.PlayerId], speed)) continue;
+ Main.AllPlayerSpeed[pc.PlayerId] = speed;
+ pc.MarkDirtySettings();
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ sealed class Meteor : Disaster
+ {
+ public Meteor(Vector2 position, NaturalDisaster naturalDisaster) : base(position)
+ {
+ NetObject = naturalDisaster;
+ Update();
+
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ if (Vector2.Distance(pc.Pos(), this.Position) <= Range)
+ {
+ pc.Suicide(PlayerState.DeathReason.Meteor);
+ }
+ }
+ }
+
+ protected override int Duration { get; set; } = 5;
+
+ public override void Update()
+ {
+ RemoveIfExpired();
+ }
+ }
+
+ sealed class VolcanoEruption : Disaster
+ {
+ private const int Phases = 4;
+ private static OptionItem FlowStepDelay;
+ private static OptionItem DurationAfterFlowComplete;
+
+ private int Phase = 1;
+ private float Timer;
+
+ public VolcanoEruption(Vector2 position, NaturalDisaster naturalDisaster) : base(position)
+ {
+ NetObject = naturalDisaster;
+ Update();
+ }
+
+ protected override int Duration { get; set; } = (int)((Phases - 1) * FlowStepDelay.GetFloat()) + DurationAfterFlowComplete.GetInt();
+
+ public static void SetupOwnCustomOption()
+ {
+ const int id = 69_216_200;
+ Color color = Utils.GetRoleColor(CustomRoles.NDPlayer);
+
+ FlowStepDelay = new FloatOptionItem(id, "ND_VolcanoEruption.FlowStepDelay", new(0.5f, 5f, 0.5f), 1f, TabGroup.GameSettings)
+ .SetHeader(true)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Seconds);
+
+ DurationAfterFlowComplete = new IntegerOptionItem(id + 1, "ND_VolcanoEruption.DurationAfterFlowComplete", new(1, 120, 1), 10, TabGroup.GameSettings)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Seconds);
+ }
+
+ public override void Update()
+ {
+ if (RemoveIfExpired()) return;
+
+ Timer += Time.deltaTime;
+ if (Timer >= FlowStepDelay.GetFloat())
+ {
+ Timer = 0f;
+ Phase++;
+ if (Phase > Phases) return;
+
+ var newSprite = Phase switch
+ {
+ 2 => "\u2588\u2588\u2588\u2588\u2588\u2588
\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588\u2588
\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588\u2588
\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588\u2588
\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588\u2588
\u2588\u2588\u2588\u2588\u2588\u2588
",
+ 3 => "<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
",
+ 4 => "<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588<#ff6200>\u2588
",
+ _ => string.Empty
+ };
+
+ this.NetObject.RpcChangeSprite(newSprite);
+ }
+
+ var range = Range - (Phases - Phase) * 0.25f;
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ if (Vector2.Distance(pc.Pos(), this.Position) <= range)
+ {
+ pc.Suicide(PlayerState.DeathReason.Lava);
+ }
+ }
+ }
+ }
+
+ sealed class Tornado : Disaster
+ {
+ private static OptionItem DurationOpt;
+ private float Angle = Random.Range(0, 2 * Mathf.PI);
+ private int Count = 2;
+
+ private long LastAngleChange = Utils.TimeStamp;
+
+ public Tornado(Vector2 position, NaturalDisaster naturalDisaster) : base(position)
+ {
+ NetObject = naturalDisaster;
+ Update();
+ }
+
+ protected override int Duration { get; set; } = DurationOpt.GetInt();
+
+ public static void SetupOwnCustomOption()
+ {
+ const int id = 69_216_300;
+ Color color = Utils.GetRoleColor(CustomRoles.NDPlayer);
+
+ DurationOpt = new IntegerOptionItem(id, "ND_Tornado.DurationOpt", new(1, 120, 1), 40, TabGroup.GameSettings)
+ .SetHeader(true)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Seconds);
+ }
+
+ public override void Update()
+ {
+ if (RemoveIfExpired()) return;
+
+ const float eyeRange = Range / 3f;
+ const float dragRange = Range * 1.5f;
+
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ var pos = pc.Pos();
+ switch (Vector2.Distance(pos, this.Position))
+ {
+ case <= eyeRange:
+ pc.Suicide(PlayerState.DeathReason.Tornado);
+ continue;
+ case <= dragRange:
+ Vector2 direction = (this.Position - pos).normalized;
+ Vector2 newPosition = pos + direction * 0.1f;
+ pc.TP(newPosition);
+ continue;
+ }
+ }
+
+ if (Count++ < 3) return;
+ Count = 0;
+
+ float angle;
+ long now = Utils.TimeStamp;
+ if (LastAngleChange + 7 <= now)
+ {
+ angle = Random.Range(0, 2 * Mathf.PI);
+ LastAngleChange = now;
+ Angle = angle;
+ }
+ else angle = Angle;
+
+ Vector2 newPos = this.Position + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * 0.1f;
+
+ this.Position = newPos;
+ this.NetObject.TP(this.Position);
+ }
+ }
+
+ sealed class Thunderstorm : Disaster
+ {
+ private static OptionItem HitFrequency;
+ private static OptionItem DurationOpt;
+
+ private long LastHit;
+
+ public Thunderstorm(Vector2 position, NaturalDisaster naturalDisaster) : base(position)
+ {
+ NetObject = naturalDisaster;
+ Update();
+
+ naturalDisaster.Despawn();
+ }
+
+ protected override int Duration { get; set; } = DurationOpt.GetInt();
+
+ public static void SetupOwnCustomOption()
+ {
+ const int id = 69_216_400;
+ Color color = Utils.GetRoleColor(CustomRoles.NDPlayer);
+
+ DurationOpt = new IntegerOptionItem(id, "ND_Thunderstorm.DurationOpt", new(1, 20, 1), 20, TabGroup.GameSettings)
+ .SetHeader(true)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Seconds);
+
+ HitFrequency = new IntegerOptionItem(id + 1, "ND_Thunderstorm.HitFrequency", new(1, 20, 1), 3, TabGroup.GameSettings)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Seconds);
+ }
+
+ public override void Update()
+ {
+ if (RemoveIfExpired()) return;
+
+ long now = Utils.TimeStamp;
+ if (now - LastHit >= HitFrequency.GetInt())
+ {
+ LastHit = now;
+ var hit = new Vector2(Random.Range(MapBounds.X.Left, MapBounds.X.Right), Random.Range(MapBounds.Y.Bottom, MapBounds.Y.Top));
+ _ = new Lightning(hit);
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ if (pc.GetPlainShipRoom() != default) continue;
+ if (Vector2.Distance(pc.Pos(), hit) <= Range)
+ {
+ pc.Suicide(PlayerState.DeathReason.Lightning);
+ }
+ }
+ }
+ }
+ }
+
+ sealed class SandStorm : Disaster
+ {
+ private static OptionItem DurationOpt;
+ private static OptionItem Vision;
+
+ private readonly HashSet AffectedPlayers = [];
+
+ public SandStorm(Vector2 position, NaturalDisaster naturalDisaster) : base(position)
+ {
+ NetObject = naturalDisaster;
+ Update();
+ }
+
+ protected override int Duration { get; set; } = DurationOpt.GetInt();
+
+ public static void SetupOwnCustomOption()
+ {
+ const int id = 69_216_500;
+ Color color = Utils.GetRoleColor(CustomRoles.NDPlayer);
+
+ DurationOpt = new IntegerOptionItem(id, "ND_SandStorm.DurationOpt", new(1, 120, 1), 30, TabGroup.GameSettings)
+ .SetHeader(true)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Seconds);
+
+ Vision = new FloatOptionItem(id + 1, "ND_SandStorm.Vision", new(0f, 1f, 0.05f), 0.2f, TabGroup.GameSettings)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Multiplier);
+ }
+
+ public override void Update()
+ {
+ if (RemoveIfExpired()) return;
+
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ if ((Vector2.Distance(pc.Pos(), this.Position) <= Range && AffectedPlayers.Add(pc.PlayerId)) || AffectedPlayers.Remove(pc.PlayerId))
+ {
+ pc.MarkDirtySettings();
+ }
+ }
+ }
+
+ protected override bool RemoveIfExpired()
+ {
+ if (base.RemoveIfExpired())
+ {
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ if (AffectedPlayers.Remove(pc.PlayerId))
+ {
+ pc.MarkDirtySettings();
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public override void ApplyOwnGameOptions(IGameOptions opt, byte id)
+ {
+ if (AffectedPlayers.Contains(id))
+ {
+ opt.SetVision(false);
+ opt.SetFloat(FloatOptionNames.CrewLightMod, Vision.GetFloat());
+ opt.SetFloat(FloatOptionNames.ImpostorLightMod, Vision.GetFloat());
+ }
+ }
+ }
+
+ sealed class Tsunami : Disaster
+ {
+ public enum MovingDirection
+ {
+ LeftToRight,
+ RightToLeft,
+ TopToBottom,
+ BottomToTop
+ }
+
+ private static OptionItem MovingSpeed;
+
+ public static readonly Dictionary Sprites = new()
+ {
+ [MovingDirection.LeftToRight] = "<#0073ff>\u2588<#0095ff>\u2588<#00ccff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588
<#0073ff>\u2588<#0095ff>\u2588<#00ccff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588
<#0073ff>\u2588<#0095ff>\u2588<#00ccff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588
<#0073ff>\u2588<#0095ff>\u2588<#00ccff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588
<#0073ff>\u2588<#0095ff>\u2588<#00ccff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588
<#0073ff>\u2588<#0095ff>\u2588<#00ccff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588
<#0073ff>\u2588<#0095ff>\u2588<#00ccff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588
<#0073ff>\u2588<#0095ff>\u2588<#00ccff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588
",
+ [MovingDirection.RightToLeft] = "<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#00ccff>\u2588<#0095ff>\u2588<#0073ff>\u2588
<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#00ccff>\u2588<#0095ff>\u2588<#0073ff>\u2588
<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#00ccff>\u2588<#0095ff>\u2588<#0073ff>\u2588
<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#00ccff>\u2588<#0095ff>\u2588<#0073ff>\u2588
<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#00ccff>\u2588<#0095ff>\u2588<#0073ff>\u2588
<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#00ccff>\u2588<#0095ff>\u2588<#0073ff>\u2588
<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#00ccff>\u2588<#0095ff>\u2588<#0073ff>\u2588
<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#9ce8ff>\u2588<#00ccff>\u2588<#0095ff>\u2588<#0073ff>\u2588
",
+ [MovingDirection.TopToBottom] = "<#003cff>\u2588<#003cff>\u2588<#003cff>\u2588<#003cff>\u2588<#003cff>\u2588<#003cff>\u2588<#003cff>\u2588<#003cff>\u2588
<#006aff>\u2588<#006aff>\u2588<#006aff>\u2588<#006aff>\u2588<#006aff>\u2588<#006aff>\u2588<#006aff>\u2588<#006aff>\u2588
<#00bbff>\u2588<#00bbff>\u2588<#00bbff>\u2588<#00bbff>\u2588<#00bbff>\u2588<#00bbff>\u2588<#00bbff>\u2588<#00bbff>\u2588
<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588
<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588
<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588
<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588
<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588<#b8e1ff>\u2588
",
+ [MovingDirection.BottomToTop] = "<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588
<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588
<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588
<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588
<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588<#c2e2ff>\u2588
<#00bfff>\u2588<#00bfff>\u2588<#00bfff>\u2588<#00bfff>\u2588<#00bfff>\u2588<#00bfff>\u2588<#00bfff>\u2588<#00bfff>\u2588
<#007bff>\u2588<#007bff>\u2588<#007bff>\u2588<#007bff>\u2588<#007bff>\u2588<#007bff>\u2588<#007bff>\u2588<#007bff>\u2588
<#0033ff>\u2588<#0033ff>\u2588<#0033ff>\u2588<#0033ff>\u2588<#0033ff>\u2588<#0033ff>\u2588<#0033ff>\u2588<#0033ff>\u2588
"
+ };
+
+ private readonly MovingDirection Direction;
+
+ public Tsunami(Vector2 position, NaturalDisaster naturalDisaster) : base(position)
+ {
+ NetObject = naturalDisaster;
+ Update();
+
+ Direction = Sprites.GetKeyByValue(NetObject.Sprite);
+ }
+
+ public static void SetupOwnCustomOption()
+ {
+ const int id = 69_216_600;
+ Color color = Utils.GetRoleColor(CustomRoles.NDPlayer);
+
+ MovingSpeed = new FloatOptionItem(id, "ND_Tsunami.MovingSpeed", new(0.05f, 0.5f, 0.05f), 0.1f, TabGroup.GameSettings)
+ .SetHeader(true)
+ .SetGameMode(CustomGameMode.NaturalDisasters)
+ .SetColor(color)
+ .SetValueFormat(OptionFormat.Multiplier);
+ }
+
+ public override void Update()
+ {
+ if (RemoveIfExpired()) return;
+
+ float speed = MovingSpeed.GetFloat();
+ Vector2 newPos = this.Position;
+ switch (Direction)
+ {
+ case MovingDirection.LeftToRight:
+ newPos.x += speed;
+ break;
+ case MovingDirection.RightToLeft:
+ newPos.x -= speed;
+ break;
+ case MovingDirection.TopToBottom:
+ newPos.y -= speed;
+ break;
+ case MovingDirection.BottomToTop:
+ newPos.y += speed;
+ break;
+ }
+
+ if (newPos.x < MapBounds.X.Left || newPos.x > MapBounds.X.Right || newPos.y < MapBounds.Y.Bottom || newPos.y > MapBounds.Y.Top)
+ {
+ Duration = 0;
+ RemoveIfExpired();
+ return;
+ }
+
+ this.Position = newPos;
+ this.NetObject.TP(this.Position);
+
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ var pos = pc.Pos();
+ var inWay = Direction switch
+ {
+ MovingDirection.LeftToRight => pos.x >= this.Position.x,
+ MovingDirection.RightToLeft => pos.x <= this.Position.x,
+ MovingDirection.TopToBottom => pos.y <= this.Position.y,
+ MovingDirection.BottomToTop => pos.y >= this.Position.y,
+ _ => false
+ };
+
+ if (Vector2.Distance(pos, this.Position) <= Range && inWay)
+ {
+ pc.Suicide(PlayerState.DeathReason.Drowned);
+ }
+ }
+ }
+ }
+
+ sealed class Sinkhole : Disaster
+ {
+ public static readonly List Sinkholes = [];
+
+ public Sinkhole(Vector2 position, NaturalDisaster naturalDisaster) : base(position)
+ {
+ NetObject = naturalDisaster;
+ Update();
+
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ if (Vector2.Distance(pc.Pos(), this.Position) <= Range)
+ {
+ pc.Suicide(PlayerState.DeathReason.Sunken);
+ }
+ }
+
+ Sinkholes.Add(position);
+ }
+
+ protected override int Duration { get; set; } = int.MaxValue;
+
+ public override void Update()
+ {
+ ActiveDisasters.Remove(this);
+ }
+
+ public static void OnFixedUpdate()
+ {
+ if (Sinkholes.Count == 0) return;
+
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ var pos = pc.Pos();
+ // ReSharper disable once ForCanBeConvertedToForeach
+ for (int i = 0; i < Sinkholes.Count; i++)
+ {
+ if (Vector2.Distance(pos, Sinkholes[i]) <= Range)
+ {
+ pc.Suicide(PlayerState.DeathReason.Sunken);
+ }
+ }
+ }
+ }
+ }
+
+ sealed class BuildingCollapse : Disaster
+ {
+ public static readonly List CollapsedRooms = [];
+ public static readonly Dictionary LastPosition = [];
+
+ public BuildingCollapse(Vector2 position, NaturalDisaster naturalDisaster) : base(position)
+ {
+ NetObject = naturalDisaster;
+ Update();
+
+ var room = naturalDisaster.playerControl.GetPlainShipRoom();
+ if (room == default) return;
+
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ if (pc.GetPlainShipRoom() == room)
+ {
+ pc.Suicide(PlayerState.DeathReason.Collapsed);
+ }
+ }
+
+ CollapsedRooms.Add(room);
+ }
+
+ public override void Update()
+ {
+ Duration = 0;
+ RemoveIfExpired();
+ }
+
+ public static void OnFixedUpdate()
+ {
+ if (CollapsedRooms.Count == 0) return;
+
+ foreach (var pc in Main.AllAlivePlayerControls)
+ {
+ if (CollapsedRooms.Find(x => x == pc.GetPlainShipRoom()))
+ {
+ if (LastPosition.TryGetValue(pc.PlayerId, out var lastPos)) pc.TP(lastPos);
+ else pc.Suicide(PlayerState.DeathReason.Collapsed);
+ }
+ else LastPosition[pc.PlayerId] = pc.Pos();
+ }
+ }
+ }
+ }
+}
+
+/*
+- Earthquake: The ground shakes, making it hard to move.
+- Meteor: A meteor falls from the sky, killing players in a certain range.
+- Volcano Eruption: Lava flows from one spot, killing players.
+- Tornado: Players are sucked into the tornado and die.
+- Thunderstorm: Lightning strikes outside of rooms, killing players that are hit by it.
+- Sandstorm: Sand blows, making it hard to see.
+- Tsunami: A giant wave comes moving in a specific direction, drowning players that are in the way.
+- Sinkhole: The ground collapses, killing players in range and any players that enter this range in the future.
+- Building Collapse: A building on the ship collapses, killing everyone inside it. Collapsed rooms cannot be entered in the future.
+*/
\ No newline at end of file
diff --git a/Modules/CustomNetObject.cs b/Modules/CustomNetObject.cs
index 53b1dcb0f..3817068af 100644
--- a/Modules/CustomNetObject.cs
+++ b/Modules/CustomNetObject.cs
@@ -5,7 +5,6 @@ using EHR.Crewmate;
using EHR.Modules;
using Hazel;
using InnerNet;
-using TMPro;
using UnityEngine;
@@ -18,18 +17,20 @@ namespace EHR
{
public static readonly List AllObjects = [];
private static int MaxId = -1;
+ private readonly HashSet HiddenList = [];
protected int Id;
public PlayerControl playerControl;
private float PlayerControlTimer;
- protected HashSet HiddenList = [];
public Vector2 Position;
- private string Sprite;
-/*
- protected void RpcChangeSprite(string sprite)
+ public string Sprite;
+
+ public void RpcChangeSprite(string sprite)
{
+ if (this is not DisasterWarningTimer) Logger.Info($" Change Custom Net Object {this.GetType().Name} (ID {Id}) sprite", "CNO.RpcChangeSprite");
Sprite = sprite;
- LateTask.New(() => {
+ LateTask.New(() =>
+ {
playerControl.RawSetName(sprite);
var name = PlayerControl.LocalPlayer.Data.Outfits[PlayerOutfitType.Default].PlayerName;
var colorId = PlayerControl.LocalPlayer.Data.Outfits[PlayerOutfitType.Default].ColorId;
@@ -72,7 +73,6 @@ namespace EHR
sender.SendMessage();
}, 0f);
}
-*/
public void TP(Vector2 position)
{
@@ -94,15 +94,13 @@ namespace EHR
HiddenList.Add(player.PlayerId);
if (player.AmOwner)
{
- LateTask.New(() =>
- {
- playerControl.transform.FindChild("Names").FindChild("NameText_TMP").gameObject.SetActive(false);
- }, 0.1f);
+ LateTask.New(() => playerControl.transform.FindChild("Names").FindChild("NameText_TMP").gameObject.SetActive(false), 0.1f);
playerControl.Visible = false;
return;
}
- LateTask.New(() => {
+ LateTask.New(() =>
+ {
CustomRpcSender sender = CustomRpcSender.Create("FixModdedClientCNOText", sendOption: SendOption.Reliable);
sender.AutoStartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.FixModdedClientCNO, player.GetClientId())
.WriteNetObject(playerControl)
@@ -111,8 +109,6 @@ namespace EHR
sender.SendMessage();
}, 0.4f);
-
-
MessageWriter writer = MessageWriter.Get();
writer.StartMessage(6);
writer.Write(AmongUsClient.Instance.GameId);
@@ -127,6 +123,13 @@ namespace EHR
protected virtual void OnFixedUpdate()
{
+ if (this.playerControl.GetRealName() == "Player")
+ {
+ Logger.Warn($" Despawn Custom Net Object {this.GetType().Name} (ID {Id}) because it is bugged", "CNO.OnFixedUpdate");
+ Despawn();
+ return;
+ }
+
PlayerControlTimer += Time.fixedDeltaTime;
if (PlayerControlTimer > 20f)
{
@@ -208,7 +211,7 @@ namespace EHR
sender.SendMessage();
}, 0.2f);
LateTask.New(() => oldPlayerControl.Despawn(), 0.3f);
- // playerControl.cosmetics.currentBodySprite.BodySprite.color = Color.clear;
+ //playerControl.cosmetics.currentBodySprite.BodySprite.color = Color.clear;
//playerControl.cosmetics.colorBlindText.color = Color.clear;
foreach (var pc in Main.AllPlayerControls)
{
@@ -243,11 +246,16 @@ namespace EHR
{
Hide(pc);
}
- LateTask.New(() => { // Fix for host
+
+ LateTask.New(() =>
+ {
+ // Fix for Host
if (!HiddenList.Contains(PlayerControl.LocalPlayer.PlayerId))
playerControl.transform.FindChild("Names").FindChild("NameText_TMP").gameObject.SetActive(true);
}, 0.1f);
- LateTask.New(() => { // Fix for Modded
+ LateTask.New(() =>
+ {
+ // Fix for Non-Host Modded
foreach (var visiblePC in Main.AllPlayerControls.ExceptBy(HiddenList, x => x.PlayerId))
{
CustomRpcSender sender = CustomRpcSender.Create("FixModdedClientCNOText", sendOption: SendOption.Reliable);
@@ -260,14 +268,14 @@ namespace EHR
}, 0.4f);
/*
- if (this is TrapArea trapArea)
- {
- foreach (var pc in PlayerControl.AllPlayerControls)
- {
- if (!trapArea.VisibleList.Contains(pc.PlayerId))
- Hide(pc);
- }
- }
+ if (this is TrapArea trapArea)
+ {
+ foreach (var pc in PlayerControl.AllPlayerControls)
+ {
+ if (!trapArea.VisibleList.Contains(pc.PlayerId))
+ Hide(pc);
+ }
+ }
*/
PlayerControlTimer = 0f;
}
@@ -354,7 +362,7 @@ namespace EHR
Position = position;
PlayerControlTimer = 0f;
//playerControl.cosmetics.currentBodySprite.BodySprite.color = Color.clear;
- // playerControl.cosmetics.colorBlindText.color = Color.clear;
+ // playerControl.cosmetics.colorBlindText.color = Color.clear;
Sprite = sprite;
++MaxId;
Id = MaxId;
@@ -388,13 +396,9 @@ namespace EHR
sender.SendMessage();
}, 0.1f);
}
-
- LateTask.New(() => { // Fix for host
- playerControl.transform.FindChild("Names").FindChild("NameText_TMP").gameObject.SetActive(true);
- }, 0.1f);
- LateTask.New(() => { // Fix for Modded
- Utils.SendRPC(CustomRPC.FixModdedClientCNO, playerControl);
- }, 0.4f);
+
+ LateTask.New(() => playerControl.transform.FindChild("Names").FindChild("NameText_TMP").gameObject.SetActive(true), 0.1f); // Fix for Host
+ LateTask.New(() => Utils.SendRPC(CustomRPC.FixModdedClientCNO, playerControl), 0.4f); // Fix for Non-Host Modded
}
public static void FixedUpdate() => AllObjects.ToArray().Do(x => x.OnFixedUpdate());
@@ -412,8 +416,6 @@ namespace EHR
Utils.ThrowException(e);
}
}
-
-
}
/*
@@ -625,8 +627,85 @@ namespace EHR
{
public SoulObject(Vector2 position, PlayerControl whisperer)
{
- CreateNetObject("\u2588\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588\u2588
\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588
<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#cfcfcf>\u2588<#cfcfcf>\u2588
<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588
\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588
\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588\u2588<#fcfcfc>\u2588
", position);
+ CreateNetObject("\u2588\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588\u2588
\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588
<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#cfcfcf>\u2588<#cfcfcf>\u2588
<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588
\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588
\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588<#fcfcfc>\u2588\u2588<#fcfcfc>\u2588
", position);
Main.AllAlivePlayerControls.Without(whisperer).Do(Hide);
}
}
+
+ internal sealed class DisasterWarningTimer : CustomNetObject
+ {
+ public DisasterWarningTimer(Vector2 position, float time, string disaster)
+ {
+ CreateNetObject($"{Math.Ceiling(time):N0}\n{disaster}", position);
+ Disaster = disaster;
+ Timer = time;
+ }
+
+ private string Disaster { get; }
+ private float Timer { get; set; }
+ private int Time => (int)Math.Ceiling(Timer);
+
+ protected override void OnFixedUpdate()
+ {
+ base.OnFixedUpdate();
+ var oldTime = Time;
+ Timer -= UnityEngine.Time.fixedDeltaTime;
+ if (Time != oldTime)
+ {
+ RpcChangeSprite($"{Time:N0}\n{Disaster}");
+ }
+ }
+ }
+
+ internal sealed class NaturalDisaster : CustomNetObject
+ {
+ public NaturalDisaster(Vector2 position, float time, string sprite, string disasterName)
+ {
+ WarningTimer = new(position, time, Translator.GetString($"ND_{disasterName}"));
+ SpawnTimer = time;
+ Sprite = sprite;
+ DisasterName = disasterName;
+ }
+
+ public string DisasterName { get; }
+ public float SpawnTimer { get; private set; }
+ private DisasterWarningTimer WarningTimer { get; }
+
+ private void RemoveWarningAndSpawn()
+ {
+ WarningTimer.Despawn();
+ CreateNetObject(Sprite, WarningTimer.Position);
+ }
+
+ public void Update()
+ {
+ if (float.IsNaN(SpawnTimer)) return;
+ SpawnTimer -= Time.fixedDeltaTime;
+ if (SpawnTimer <= 0f)
+ {
+ RemoveWarningAndSpawn();
+ SpawnTimer = float.NaN;
+ }
+ }
+ }
+
+ internal sealed class Lightning : CustomNetObject
+ {
+ private float Timer = 5f;
+
+ public Lightning(Vector2 position)
+ {
+ CreateNetObject("\u2588\u2588\u2588\u2588\u2588<#c6c7c3>\u2588
\u2588<#c6c7c3>\u2588\u2588\u2588<#c6c7c3>\u2588\u2588
<#c6c7c3>\u2588\u2588<#fffb00>\u2588<#fffb00>\u2588\u2588\u2588
\u2588\u2588<#fffb00>\u2588<#fffb00>\u2588\u2588<#c6c7c3>\u2588
<#c6c7c3>\u2588\u2588\u2588\u2588\u2588\u2588
\u2588\u2588<#c6c7c3>\u2588<#c6c7c3>\u2588\u2588\u2588
", position);
+ }
+
+ protected override void OnFixedUpdate()
+ {
+ base.OnFixedUpdate();
+ Timer -= Time.fixedDeltaTime;
+ if (Timer <= 0)
+ {
+ Despawn();
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/Modules/CustomRoleSelector.cs b/Modules/CustomRoleSelector.cs
index 9ddea84d5..5676fab91 100644
--- a/Modules/CustomRoleSelector.cs
+++ b/Modules/CustomRoleSelector.cs
@@ -62,6 +62,9 @@ internal static class CustomRoleSelector
case CustomGameMode.CaptureTheFlag:
AssignRoleToEveryone(CustomRoles.CTFPlayer);
return;
+ case CustomGameMode.NaturalDisasters:
+ AssignRoleToEveryone(CustomRoles.NDPlayer);
+ return;
case CustomGameMode.HideAndSeek:
HnSManager.AssignRoles();
RoleResult = HnSManager.PlayerRoles.ToDictionary(x => Utils.GetPlayerById(x.Key), x => x.Value.Role);
diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs
index 8764a4ae6..2c04d6961 100644
--- a/Modules/CustomRolesHelper.cs
+++ b/Modules/CustomRolesHelper.cs
@@ -55,7 +55,8 @@ internal static class CustomRolesHelper
CustomRoles.Tasker or
CustomRoles.Potato or
CustomRoles.Runner or
- CustomRoles.CTFPlayer;
+ CustomRoles.CTFPlayer or
+ CustomRoles.NDPlayer;
public static RoleBase GetRoleClass(this CustomRoles role)
{
@@ -377,6 +378,8 @@ internal static class CustomRolesHelper
CustomRoles.Runner => RoleTypes.Crewmate,
// Capture The Flag
CustomRoles.CTFPlayer => RoleTypes.Shapeshifter,
+ // Natural Disasters
+ CustomRoles.NDPlayer => RoleTypes.Crewmate,
// Standard
CustomRoles.Executioner => Executioner.CRoleChangeRoles[Executioner.ChangeRolesAfterTargetKilled.GetValue()].GetDYRole(),
CustomRoles.Sheriff => UsePets && Sheriff.UsePet.GetBool() ? RoleTypes.GuardianAngel : RoleTypes.Impostor,
diff --git a/Modules/DisableDevice.cs b/Modules/DisableDevice.cs
index bb2d8ccd1..f8be6728a 100644
--- a/Modules/DisableDevice.cs
+++ b/Modules/DisableDevice.cs
@@ -8,7 +8,7 @@ using UnityEngine;
namespace EHR;
//参考元 : https://github.com/ykundesu/SuperNewRoles/blob/master/SuperNewRoles/Mode/SuperHostRoles/BlockTool.cs
-class DisableDevice
+static class DisableDevice
{
private static readonly List DesyncComms = [];
private static int frame;
diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs
index cd8fab9eb..60ea496ed 100644
--- a/Modules/ExtendedPlayerControl.cs
+++ b/Modules/ExtendedPlayerControl.cs
@@ -400,6 +400,13 @@ static class ExtendedPlayerControl
}
pc.Kill(pc);
+
+ if (Options.CurrentGameMode == CustomGameMode.NaturalDisasters)
+ {
+ var message = GetString($"ND_DRLaughMessage.{deathReason}");
+ message = ColorString(NaturalDisasters.DeathReasonColor(deathReason), message);
+ LateTask.New(() => pc.Notify(message, 20f), 2f, $"{pc.GetRealName()} died with the reason {deathReason}");
+ }
}
public static void SetKillCooldown(this PlayerControl player, float time = -1f, PlayerControl target = null, bool forceAnime = false)
@@ -546,7 +553,7 @@ static class ExtendedPlayerControl
public static string GetNameWithRole(this PlayerControl player, bool forUser = false)
{
- return $"{player?.Data?.PlayerName}" + (GameStates.IsInGame && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag ? $" ({player?.GetAllRoleName(forUser).RemoveHtmlTags().Replace('\n', ' ')})" : string.Empty);
+ return $"{player?.Data?.PlayerName}" + (GameStates.IsInGame && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters ? $" ({player?.GetAllRoleName(forUser).RemoveHtmlTags().Replace('\n', ' ')})" : string.Empty);
}
public static string GetRoleColorCode(this PlayerControl player)
@@ -659,7 +666,7 @@ static class ExtendedPlayerControl
switch (Options.CurrentGameMode)
{
- case CustomGameMode.HotPotato or CustomGameMode.MoveAndStop:
+ case CustomGameMode.HotPotato or CustomGameMode.MoveAndStop or CustomGameMode.NaturalDisasters:
case CustomGameMode.Speedrun when !SpeedrunManager.CanKill.Contains(pc.PlayerId):
return false;
case CustomGameMode.CaptureTheFlag:
@@ -725,6 +732,8 @@ static class ExtendedPlayerControl
CustomRoles.Runner => false,
// Capture The Flag
CustomRoles.CTFPlayer => false,
+ // Natural Disasters
+ CustomRoles.NDPlayer => false,
_ => Main.PlayerStates.TryGetValue(pc.PlayerId, out var state) && state.Role.CanUseImpostorVentButton(pc)
};
diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs
index 3b3142f53..de7b8d953 100644
--- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs
+++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs
@@ -170,6 +170,10 @@ public sealed class PlayerGameOptionsSender(PlayerControl player) : GameOptionsS
CTFManager.ApplyGameOptions(opt);
SetMaxVision();
break;
+ case CustomGameMode.NaturalDisasters:
+ SetMaxVision();
+ NaturalDisasters.ApplyGameOptions(opt, player.PlayerId);
+ break;
case CustomGameMode.Speedrun:
case CustomGameMode.HotPotato:
case CustomGameMode.MoveAndStop:
diff --git a/Modules/GameState.cs b/Modules/GameState.cs
index f7bc4bf7a..e45d63e14 100644
--- a/Modules/GameState.cs
+++ b/Modules/GameState.cs
@@ -58,6 +58,15 @@ public class PlayerState(byte playerId)
Assumed,
Negotiation,
+ // Natural Disasters
+ Meteor,
+ Lava,
+ Tornado,
+ Lightning,
+ Drowned,
+ Sunken,
+ Collapsed,
+
etc = -1
}
diff --git a/Modules/NameColorManager.cs b/Modules/NameColorManager.cs
index 943490067..9fd3e018c 100644
--- a/Modules/NameColorManager.cs
+++ b/Modules/NameColorManager.cs
@@ -42,6 +42,7 @@ public static class NameColorManager
case CustomGameMode.FFA when FFAManager.FFATeamMode.GetBool():
color = FFAManager.TeamColors[FFAManager.PlayerTeams[target.PlayerId]];
return true;
+ case CustomGameMode.NaturalDisasters:
case CustomGameMode.MoveAndStop:
color = "#ffffff";
return true;
diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs
index ffa4bc687..d40df642d 100644
--- a/Modules/OptionHolder.cs
+++ b/Modules/OptionHolder.cs
@@ -1,7 +1,10 @@
using System;
+using System.Collections;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Reflection;
+using System.Text;
using EHR.AddOns;
using EHR.AddOns.GhostRoles;
using EHR.Modules;
@@ -24,6 +27,7 @@ public enum CustomGameMode
HideAndSeek = 0x06,
Speedrun = 0x07,
CaptureTheFlag = 0x08,
+ NaturalDisasters = 0x09,
All = int.MaxValue
}
@@ -57,7 +61,8 @@ public static class Options
"HotPotato",
"HideAndSeek",
"Speedrun",
- "CaptureTheFlag"
+ "CaptureTheFlag",
+ "NaturalDisasters"
];
private static Dictionary roleCounts;
@@ -777,6 +782,7 @@ public static class Options
5 => CustomGameMode.HideAndSeek,
6 => CustomGameMode.Speedrun,
7 => CustomGameMode.CaptureTheFlag,
+ 8 => CustomGameMode.NaturalDisasters,
_ => CustomGameMode.Standard
};
@@ -803,7 +809,7 @@ public static class Options
// Used for generating the table of roles for the README
try
{
- var sb = new System.Text.StringBuilder();
+ var sb = new StringBuilder();
var grouped = Enum.GetValues().GroupBy(x =>
{
if (x is CustomRoles.GM or CustomRoles.Philantropist or CustomRoles.Konan or CustomRoles.NotAssigned or CustomRoles.LovingCrewmate or CustomRoles.LovingImpostor or CustomRoles.Convict || x.IsForOtherGameMode() || x.IsVanilla() || x.ToString().Contains("EHR") || HnSManager.AllHnSRoles.Contains(x)) return 4;
@@ -831,8 +837,8 @@ public static class Options
}
const string path = "./roles.txt";
- if (!System.IO.File.Exists(path)) System.IO.File.Create(path).Close();
- System.IO.File.WriteAllText(path, sb.ToString());
+ if (!File.Exists(path)) File.Create(path).Close();
+ File.WriteAllText(path, sb.ToString());
}
catch (Exception e)
{
@@ -903,7 +909,7 @@ public static class Options
return CustomRoleSpawnChances.TryGetValue(role, out var option) ? option.GetValue() /* / 10f */ : roleSpawnChances[role];
}
- private static System.Collections.IEnumerator Load()
+ private static IEnumerator Load()
{
LoadingPercentage = 0;
MainLoadingText = "Building system settings";
@@ -1268,6 +1274,8 @@ public static class Options
HnSManager.SetupCustomOption();
// Capture The Flag
CTFManager.SetupCustomOption();
+ // Natural Disasters
+ NaturalDisasters.SetupCustomOption();
yield return null;
diff --git a/Modules/Utils.cs b/Modules/Utils.cs
index 9ae9d42e7..63c36cccd 100644
--- a/Modules/Utils.cs
+++ b/Modules/Utils.cs
@@ -649,6 +649,7 @@ public static class Utils
case CustomGameMode.HotPotato: return false;
case CustomGameMode.Speedrun: return true;
case CustomGameMode.CaptureTheFlag: return false;
+ case CustomGameMode.NaturalDisasters: return false;
case CustomGameMode.HideAndSeek: return HnSManager.HasTasks(p);
}
@@ -841,7 +842,7 @@ public static class Utils
public static bool IsRoleTextEnabled(PlayerControl __instance)
{
- if (Options.CurrentGameMode == CustomGameMode.CaptureTheFlag) return false;
+ if (Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters) return false;
if (__instance.PlayerId == PlayerControl.LocalPlayer.PlayerId || Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.SoloKombat or CustomGameMode.MoveAndStop or CustomGameMode.HotPotato or CustomGameMode.Speedrun || (Options.CurrentGameMode == CustomGameMode.HideAndSeek && HnSManager.IsRoleTextEnabled(PlayerControl.LocalPlayer, __instance)) || Main.VisibleTasksCount && PlayerControl.LocalPlayer.Data.IsDead && Options.GhostCanSeeOtherRoles.GetBool() || PlayerControl.LocalPlayer.Is(CustomRoles.Mimic) && Main.VisibleTasksCount && __instance.Data.IsDead && Options.MimicCanSeeDeadRoles.GetBool()) return true;
switch (__instance.GetCustomRole())
@@ -1342,6 +1343,7 @@ public static class Utils
}
break;
+ case CustomGameMode.NaturalDisasters:
case CustomGameMode.CaptureTheFlag:
case CustomGameMode.Speedrun:
case CustomGameMode.HotPotato:
@@ -1684,6 +1686,7 @@ public static class Utils
CustomGameMode.HotPotato => $"{modeText}\r\n{name}",
CustomGameMode.HideAndSeek => $"{modeText}\r\n{name}",
CustomGameMode.CaptureTheFlag => $"{modeText}\r\n{name}",
+ CustomGameMode.NaturalDisasters => $"{modeText}\r\n{name}",
CustomGameMode.Speedrun => ColorString(GetRoleColor(CustomRoles.Speedrunner), $"{modeText}\r\n") + name,
_ => name
};
@@ -1945,6 +1948,9 @@ public static class Utils
case CustomGameMode.CaptureTheFlag:
SelfSuffix.Append(CTFManager.GetSuffixText(seer, seer));
break;
+ case CustomGameMode.NaturalDisasters:
+ SelfSuffix.Append(NaturalDisasters.GetSuffixText());
+ break;
}
}
@@ -1955,7 +1961,7 @@ public static class Utils
if (Options.CurrentGameMode == CustomGameMode.FFA && FFAManager.FFATeamMode.GetBool())
SeerRealName = SeerRealName.ApplyNameColorData(seer, seer, isForMeeting);
- if (!isForMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool() && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag)
+ if (!isForMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool() && Options.CurrentGameMode is not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters)
{
var team = CustomTeamManager.GetCustomTeam(seer.PlayerId);
if (team != null)
@@ -2192,7 +2198,7 @@ public static class Utils
? $"{target.GetDisplayRoleName(seeTargetBetrayalAddons: shouldSeeTargetAddons)}{GetProgressText(target)}\r\n"
: string.Empty;
- if (Options.CurrentGameMode == CustomGameMode.CaptureTheFlag) TargetRoleText = string.Empty;
+ if (Options.CurrentGameMode is CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters) TargetRoleText = string.Empty;
if (!GameStates.IsLobby)
{
@@ -2470,7 +2476,7 @@ public static class Utils
CustomRoles.Sentinel => Sentinel.PatrolCooldown.GetInt(),
CustomRoles.Druid => Druid.VentCooldown.GetInt(),
CustomRoles.Catcher => Catcher.AbilityCooldown.GetInt(),
- CustomRoles.Sentry => EHR.Impostor.Sentry.ShowInfoCooldown.GetInt(),
+ CustomRoles.Sentry => Impostor.Sentry.ShowInfoCooldown.GetInt(),
CustomRoles.ToiletMaster => ToiletMaster.AbilityCooldown.GetInt(),
CustomRoles.Sniper => Options.DefaultShapeshiftCooldown.GetInt(),
CustomRoles.Assassin => Assassin.AssassinateCooldownOpt.GetInt(),
@@ -2709,7 +2715,7 @@ public static class Utils
Amnesiac.OnAnyoneDeath(target);
Dad.OnAnyoneDeath(target);
Whisperer.OnAnyoneDied(target);
- EHR.Impostor.Sentry.OnAnyoneMurder(target);
+ Impostor.Sentry.OnAnyoneMurder(target);
if (QuizMaster.On) QuizMaster.Data.NumPlayersDeadThisRound++;
@@ -2882,7 +2888,11 @@ public static class Utils
break;
case CustomGameMode.HotPotato:
int time = HotPotatoManager.GetSurvivalTime(id);
- summary = $"{ColorString(Main.PlayerColors[id], name)} - <#e8cd46>Survived: <#ffffff>{(time == 0 ? "Until The End" : $"{time}s")} ({GetVitalText(id, true)})";
+ summary = $"{ColorString(Main.PlayerColors[id], name)} - <#e8cd46>{GetString("SurvivedTimePrefix")}: <#ffffff>{(time == 0 ? $"{GetString("SurvivedUntilTheEnd")}" : $"{time}s")} ({GetVitalText(id, true)})";
+ break;
+ case CustomGameMode.NaturalDisasters:
+ int time2 = NaturalDisasters.GetSurvivalTime(id);
+ summary = $"{ColorString(Main.PlayerColors[id], name)} - <#e8cd46>{GetString("SurvivedTimePrefix")}: <#ffffff>{(time2 == 0 ? $"{GetString("SurvivedUntilTheEnd")}" : $"{time2}s")} ({GetVitalText(id, true)})";
break;
case CustomGameMode.CaptureTheFlag:
summary = $"{ColorString(Main.PlayerColors[id], name)}: {CTFManager.GetStatistics(id)}";
diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs
index 88f9eb405..3eef73538 100644
--- a/Patches/CheckGameEndPatch.cs
+++ b/Patches/CheckGameEndPatch.cs
@@ -376,6 +376,7 @@ class GameEndChecker
public static void SetPredicateToSpeedrun() => Predicate = new SpeedrunGameEndPredicate();
public static void SetPredicateToHideAndSeek() => Predicate = new HideAndSeekGameEndPredicate();
public static void SetPredicateToCaptureTheFlag() => Predicate = new CaptureTheFlagGameEndPredicate();
+ public static void SetPredicateToNaturalDisasters() => Predicate = new NaturalDisastersGameEndPredicate();
class NormalGameEndPredicate : GameEndPredicate
{
@@ -735,6 +736,36 @@ class GameEndChecker
}
}
+ class NaturalDisastersGameEndPredicate : GameEndPredicate
+ {
+ public override bool CheckForEndGame(out GameOverReason reason)
+ {
+ reason = GameOverReason.ImpostorByKill;
+ return WinnerIds.Count <= 0 && CheckGameEndByLivingPlayers(out reason);
+ }
+
+ private static bool CheckGameEndByLivingPlayers(out GameOverReason reason)
+ {
+ reason = GameOverReason.ImpostorByKill;
+
+ switch (Main.AllAlivePlayerControls.Length)
+ {
+ case 1:
+ var winner = Main.AllAlivePlayerControls[0];
+ Logger.Info($"Winner: {winner.GetRealName().RemoveHtmlTags()}", "NaturalDisasters");
+ WinnerIds = [winner.PlayerId];
+ Main.DoBlockNameChange = true;
+ return true;
+ case 0:
+ ResetAndSetWinner(CustomWinner.Error);
+ Logger.Warn("No players alive. Force ending the game", "NaturalDisasters");
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+
public abstract class GameEndPredicate
{
/// Checks the game ending condition and stores the value in CustomWinnerHolder.
diff --git a/Patches/GameOptionsPatch.cs b/Patches/GameOptionsPatch.cs
index 69d20bf3e..4bb32a418 100644
--- a/Patches/GameOptionsPatch.cs
+++ b/Patches/GameOptionsPatch.cs
@@ -57,6 +57,7 @@ class SetRecommendationsPatch
{
case CustomGameMode.SoloKombat:
case CustomGameMode.FFA:
+ case CustomGameMode.NaturalDisasters:
case CustomGameMode.HotPotato:
case CustomGameMode.CaptureTheFlag:
__instance.CrewLightMod = __instance.ImpostorLightMod = 1.25f;
diff --git a/Patches/HudPatch.cs b/Patches/HudPatch.cs
index 21f0a35b9..8365b18b4 100644
--- a/Patches/HudPatch.cs
+++ b/Patches/HudPatch.cs
@@ -233,6 +233,7 @@ class HudManagerPatch
CustomGameMode.MoveAndStop when player.IsHost() => MoveAndStopManager.HUDText,
CustomGameMode.HotPotato when player.IsHost() => HotPotatoManager.GetSuffixText(player.PlayerId),
CustomGameMode.HideAndSeek when player.IsHost() => HnSManager.GetSuffixText(player, player, isHUD: true),
+ CustomGameMode.NaturalDisasters => NaturalDisasters.GetSuffixText(),
CustomGameMode.Standard => state.Role.GetSuffix(player, player, true, GameStates.IsMeeting) + GetAddonSuffixes(),
_ => string.Empty
};
@@ -420,6 +421,7 @@ class SetHudActivePatch
case CustomGameMode.MoveAndStop:
case CustomGameMode.HotPotato:
case CustomGameMode.Speedrun:
+ case CustomGameMode.NaturalDisasters:
__instance.ReportButton?.ToggleVisible(false);
__instance.KillButton?.ToggleVisible(false);
__instance.SabotageButton?.ToggleVisible(false);
@@ -745,6 +747,13 @@ class TaskPanelBehaviourPatch
AllText += $"\r\n{SpeedrunManager.GetTaskBarText()}";
break;
+
+ case CustomGameMode.NaturalDisasters:
+
+ AllText += Main.AllPlayerControls.Select(x => (pc: x, alive: x.IsAlive(), time: NaturalDisasters.GetSurvivalTime(x.PlayerId))).OrderByDescending(x => x.time).ThenByDescending(x => x.alive).Aggregate("", (s, x) => $"{s}\r\n{x.pc.PlayerId.ColoredPlayerName()} - {(x.alive ? $"<#00ff00>{GetString("Alive")}" : $"{GetString("Dead")}: {string.Format(GetString("SurvivalTime"), x.time)}")}");
+
+ AllText += "";
+ break;
}
__instance.taskText.text = AllText;
diff --git a/Patches/IntroPatch.cs b/Patches/IntroPatch.cs
index 8ac1b1930..83569f1cc 100644
--- a/Patches/IntroPatch.cs
+++ b/Patches/IntroPatch.cs
@@ -1,9 +1,11 @@
using System;
using System.Linq;
+using System.Text;
using AmongUs.GameOptions;
using EHR.Modules;
using EHR.Neutral;
using HarmonyLib;
+using Il2CppSystem.Collections.Generic;
using UnityEngine;
using static EHR.Translator;
@@ -87,6 +89,16 @@ class SetUpRoleTextPatch
__instance.RoleBlurbText.text = GetString("CTFPlayerInfo");
break;
}
+ case CustomGameMode.NaturalDisasters:
+ {
+ var color = ColorUtility.TryParseHtmlString("#03fc4a", out var c) ? c : new(255, 255, 255, 255);
+ __instance.YouAreText.color = color;
+ __instance.RoleText.text = GetString("NDPlayer");
+ __instance.RoleText.color = color;
+ __instance.RoleBlurbText.color = color;
+ __instance.RoleBlurbText.text = GetString("NDPlayerInfo");
+ break;
+ }
default:
{
CustomRoles role = lp.GetCustomRole();
@@ -127,7 +139,7 @@ class CoBeginPatch
{
public static void Prefix()
{
- var sb = new System.Text.StringBuilder();
+ var sb = new StringBuilder();
sb.Append("------------Display Names------------\n");
foreach (PlayerControl pc in Main.AllPlayerControls)
{
@@ -189,7 +201,7 @@ class CoBeginPatch
[HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.BeginCrewmate))]
class BeginCrewmatePatch
{
- public static bool Prefix(IntroCutscene __instance, ref Il2CppSystem.Collections.Generic.List teamToDisplay)
+ public static bool Prefix(IntroCutscene __instance, ref List teamToDisplay)
{
CustomRoles role = PlayerControl.LocalPlayer.GetCustomRole();
if (PlayerControl.LocalPlayer.Is(CustomRoleTypes.Neutral) && !role.IsMadmate())
@@ -259,7 +271,7 @@ class BeginCrewmatePatch
return true;
}
- public static void Postfix(IntroCutscene __instance, ref Il2CppSystem.Collections.Generic.List teamToDisplay)
+ public static void Postfix(IntroCutscene __instance, ref List teamToDisplay)
{
CustomRoles role = PlayerControl.LocalPlayer.GetCustomRole();
@@ -579,6 +591,15 @@ class BeginCrewmatePatch
__instance.ImpostorText.text = GetString("CTFPlayerInfo");
break;
}
+ case CustomGameMode.NaturalDisasters:
+ {
+ __instance.TeamTitle.text = "" + GetString("NDPlayer") + "";
+ __instance.TeamTitle.color = __instance.BackgroundBar.material.color = new Color32(3, 252, 74, byte.MaxValue);
+ PlayerControl.LocalPlayer.Data.Role.IntroSound = GetIntroSound(RoleTypes.GuardianAngel);
+ __instance.ImpostorText.gameObject.SetActive(true);
+ __instance.ImpostorText.text = GetString("NDPlayerInfo");
+ break;
+ }
}
// if (Input.GetKey(KeyCode.RightShift))
@@ -644,7 +665,7 @@ class BeginCrewmatePatch
[HarmonyPatch(typeof(IntroCutscene), nameof(IntroCutscene.BeginImpostor))]
class BeginImpostorPatch
{
- public static bool Prefix(IntroCutscene __instance, ref Il2CppSystem.Collections.Generic.List yourTeam)
+ public static bool Prefix(IntroCutscene __instance, ref List yourTeam)
{
var role = PlayerControl.LocalPlayer.GetCustomRole();
if (PlayerControl.LocalPlayer.Is(CustomRoles.Madmate) || role.IsMadmate())
@@ -679,7 +700,7 @@ class BeginImpostorPatch
return true;
}
- public static void Postfix(IntroCutscene __instance, ref Il2CppSystem.Collections.Generic.List yourTeam)
+ public static void Postfix(IntroCutscene __instance, ref List yourTeam)
{
BeginCrewmatePatch.Postfix(__instance, ref yourTeam);
}
diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs
index 62d398921..bbe3616cf 100644
--- a/Patches/OnGameStartedPatch.cs
+++ b/Patches/OnGameStartedPatch.cs
@@ -821,6 +821,9 @@ internal class SelectRolesPatch
case CustomGameMode.CaptureTheFlag:
CTFManager.OnGameStart();
break;
+ case CustomGameMode.NaturalDisasters:
+ NaturalDisasters.OnGameStart();
+ break;
}
HudManager.Instance.SetHudActive(true);
@@ -865,6 +868,9 @@ internal class SelectRolesPatch
case CustomGameMode.CaptureTheFlag:
GameEndChecker.SetPredicateToCaptureTheFlag();
break;
+ case CustomGameMode.NaturalDisasters:
+ GameEndChecker.SetPredicateToNaturalDisasters();
+ break;
}
GameOptionsSender.AllSenders.Clear();
diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs
index 1edc19d01..07c0feef9 100644
--- a/Patches/OutroPatch.cs
+++ b/Patches/OutroPatch.cs
@@ -62,7 +62,7 @@ class EndGamePatch
if (date == DateTime.MinValue) continue;
var killerId = value.GetRealKiller();
var gmIsFM = Options.CurrentGameMode is CustomGameMode.FFA or CustomGameMode.MoveAndStop;
- var gmIsFMHH = gmIsFM || Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.HideAndSeek or CustomGameMode.Speedrun or CustomGameMode.CaptureTheFlag;
+ var gmIsFMHH = gmIsFM || Options.CurrentGameMode is CustomGameMode.HotPotato or CustomGameMode.HideAndSeek or CustomGameMode.Speedrun or CustomGameMode.CaptureTheFlag or CustomGameMode.NaturalDisasters;
sb.Append($"\n{date:T} {Main.AllPlayerNames[key]} ({(gmIsFMHH ? string.Empty : Utils.GetDisplayRoleName(key, true))}{(gmIsFM ? string.Empty : Utils.GetSubRolesText(key, summary: true))}) [{Utils.GetVitalText(key)}]");
if (killerId != byte.MaxValue && killerId != key)
sb.Append($"\n\t⇐ {Main.AllPlayerNames[killerId]} ({(gmIsFMHH ? string.Empty : Utils.GetDisplayRoleName(killerId, true))}{(gmIsFM ? string.Empty : Utils.GetSubRolesText(killerId, summary: true))})");
@@ -233,7 +233,7 @@ class SetEverythingUpPatch
{
var winnerId = CustomWinnerHolder.WinnerIds.FirstOrDefault();
__instance.BackgroundBar.material.color = new Color32(0, 255, 255, 255);
- WinnerText.text = FFAManager.FFATeamMode.GetBool() ? string.Empty : Main.AllPlayerNames[winnerId] + " wins!";
+ WinnerText.text = FFAManager.FFATeamMode.GetBool() ? string.Empty : Main.AllPlayerNames[winnerId] + GetString("Win");
WinnerText.color = Main.PlayerColors[winnerId];
goto EndOfText;
}
@@ -241,7 +241,7 @@ class SetEverythingUpPatch
{
var winnerId = CustomWinnerHolder.WinnerIds.FirstOrDefault();
__instance.BackgroundBar.material.color = new Color32(0, 255, 165, 255);
- WinnerText.text = Main.AllPlayerNames[winnerId] + " wins!";
+ WinnerText.text = Main.AllPlayerNames[winnerId] + GetString("Win");
WinnerText.color = Main.PlayerColors[winnerId];
goto EndOfText;
}
@@ -249,7 +249,7 @@ class SetEverythingUpPatch
{
var winnerId = CustomWinnerHolder.WinnerIds.FirstOrDefault();
__instance.BackgroundBar.material.color = new Color32(232, 205, 70, 255);
- WinnerText.text = Main.AllPlayerNames[winnerId] + " wins!";
+ WinnerText.text = Main.AllPlayerNames[winnerId] + GetString("Win");
WinnerText.color = Main.PlayerColors[winnerId];
goto EndOfText;
}
@@ -257,7 +257,7 @@ class SetEverythingUpPatch
{
var winnerId = CustomWinnerHolder.WinnerIds.FirstOrDefault();
__instance.BackgroundBar.material.color = Utils.GetRoleColor(CustomRoles.Speedrunner);
- WinnerText.text = Main.AllPlayerNames[winnerId] + " wins!";
+ WinnerText.text = Main.AllPlayerNames[winnerId] + GetString("Win");
WinnerText.color = Main.PlayerColors[winnerId];
goto EndOfText;
}
@@ -269,6 +269,14 @@ class SetEverythingUpPatch
WinnerText.color = winnerData.Color;
goto EndOfText;
}
+ case CustomGameMode.NaturalDisasters:
+ {
+ var winnerId = CustomWinnerHolder.WinnerIds.FirstOrDefault();
+ __instance.BackgroundBar.material.color = new Color32(3, 252, 74, 255);
+ WinnerText.text = Main.AllPlayerNames[winnerId] + GetString("Win");
+ WinnerText.color = Main.PlayerColors[winnerId];
+ goto EndOfText;
+ }
}
if (CustomWinnerHolder.WinnerTeam == CustomWinner.CustomTeam)
@@ -441,6 +449,13 @@ class SetEverythingUpPatch
sb.Append("\n\u3000 ").Append(EndGamePatch.SummaryText[id]);
break;
}
+ case CustomGameMode.NaturalDisasters:
+ {
+ var list = cloneRoles.OrderByDescending(NaturalDisasters.GetSurvivalTime);
+ foreach (var id in list.Where(EndGamePatch.SummaryText.ContainsKey))
+ sb.Append("\n\u3000 ").Append(EndGamePatch.SummaryText[id]);
+ break;
+ }
default:
{
sb.Append("\n");
diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs
index 41558ace4..dfaa266f3 100644
--- a/Patches/PlayerControlPatch.cs
+++ b/Patches/PlayerControlPatch.cs
@@ -215,6 +215,7 @@ class CheckMurderPatch
return true;
case CustomGameMode.MoveAndStop:
case CustomGameMode.HotPotato:
+ case CustomGameMode.NaturalDisasters:
return false;
case CustomGameMode.Speedrun when !SpeedrunManager.CanKill.Contains(killer.PlayerId):
return false;
@@ -842,7 +843,7 @@ class ShapeshiftPatch
case Adventurer av:
Adventurer.OnAnyoneShapeshiftLoop(av, __instance);
break;
- case EHR.Impostor.Sentry st:
+ case Impostor.Sentry st:
st.OnAnyoneShapeshiftLoop(__instance, target);
break;
}
@@ -1865,7 +1866,7 @@ class EnterVentPatch
Drainer.OnAnyoneEnterVent(pc, __instance);
Analyst.OnAnyoneEnterVent(pc);
- EHR.Impostor.Sentry.OnAnyoneEnterVent(pc);
+ Impostor.Sentry.OnAnyoneEnterVent(pc);
switch (pc.GetCustomRole())
{
@@ -1937,6 +1938,7 @@ class CoEnterVentPatch
case CustomGameMode.HotPotato:
case CustomGameMode.Speedrun:
case CustomGameMode.CaptureTheFlag:
+ case CustomGameMode.NaturalDisasters:
LateTask.New(() => __instance.RpcBootFromVent(id), 0.5f, log: false);
return true;
case CustomGameMode.HideAndSeek:
diff --git a/Patches/RandomSpawnPatch.cs b/Patches/RandomSpawnPatch.cs
index 6b3c658c5..c997852e4 100644
--- a/Patches/RandomSpawnPatch.cs
+++ b/Patches/RandomSpawnPatch.cs
@@ -43,7 +43,7 @@ class RandomSpawn
player.RpcResetAbilityCooldown();
if (Options.FixFirstKillCooldown.GetBool() && !MeetingStates.MeetingCalled) player.SetKillCooldown(Main.AllPlayerKillCooldown[player.PlayerId]);
else if (Options.StartingKillCooldown.GetInt() != 10) player.SetKillCooldown(Options.StartingKillCooldown.GetInt());
- if (!Options.RandomSpawn.GetBool() && Options.CurrentGameMode != CustomGameMode.SoloKombat) return;
+ if (!Options.RandomSpawn.GetBool() && Options.CurrentGameMode == CustomGameMode.Standard) return;
new AirshipSpawnMap().RandomTeleport(player);
Penguin.OnSpawnAirship();
}
diff --git a/Patches/ShipStatusPatch.cs b/Patches/ShipStatusPatch.cs
index 5fe6bf1de..990f764cb 100644
--- a/Patches/ShipStatusPatch.cs
+++ b/Patches/ShipStatusPatch.cs
@@ -240,7 +240,7 @@ class CloseDoorsPatch
{
public static bool Prefix( /*ShipStatus __instance, */ [HarmonyArgument(0)] SystemTypes room)
{
- bool allow = !Options.DisableSabotage.GetBool() && Options.CurrentGameMode is not CustomGameMode.SoloKombat and not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun;
+ bool allow = !Options.DisableSabotage.GetBool() && Options.CurrentGameMode is not CustomGameMode.SoloKombat and not CustomGameMode.FFA and not CustomGameMode.MoveAndStop and not CustomGameMode.HotPotato and not CustomGameMode.Speedrun and not CustomGameMode.CaptureTheFlag and not CustomGameMode.NaturalDisasters;
if (SecurityGuard.BlockSabo.Count > 0) allow = false;
if (Options.DisableCloseDoor.GetBool()) allow = false;
diff --git a/Patches/TextBoxPatch.cs b/Patches/TextBoxPatch.cs
index 7f4d3c1eb..5973d5538 100644
--- a/Patches/TextBoxPatch.cs
+++ b/Patches/TextBoxPatch.cs
@@ -168,7 +168,8 @@ class TextBoxTMPSetTextPatch
CommandInfoText.enableWordWrapping = false;
CommandInfoText.color = Color.white;
CommandInfoText.fontSize = CommandInfoText.fontSizeMax = CommandInfoText.fontSizeMin = 1.8f;
- CommandInfoText.sortingOrder = 100;
+ CommandInfoText.sortingOrder = 1000;
+ CommandInfoText.transform.SetAsLastSibling();
}
var inputForm = input.TrimStart('/');
diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json
index 06f1a7e3e..e56a68713 100644
--- a/Resources/Lang/en_US.json
+++ b/Resources/Lang/en_US.json
@@ -1789,6 +1789,20 @@
"DeathReason.Asthma": "Asthma",
"DeathReason.Assumed": "Assumed",
"DeathReason.Negotiation": "Negotiation",
+ "DeathReason.Meteor": "Meteor Strike",
+ "DeathReason.Lava": "Lava",
+ "DeathReason.Tornado": "Tornado",
+ "DeathReason.Lightning": "Lightning",
+ "DeathReason.Drowned": "Drowned",
+ "DeathReason.Sunken": "Sunken",
+ "DeathReason.Collapsed": "Collapsed",
+ "ND_DRLaughMessage.Meteor": "Meteor.... I hardly knew her!",
+ "ND_DRLaughMessage.Lava": "Lava is not the best place to swim, huh?",
+ "ND_DRLaughMessage.Tornado": "Out for a spin? I hope this tornado didn't make you dizzy!",
+ "ND_DRLaughMessage.Lightning": "You've been.... THUNDERSTRUCK!",
+ "ND_DRLaughMessage.Drowned": "You said you were thirsty, right? Here's some water!",
+ "ND_DRLaughMessage.Sunken": "Did you know that the Titanic was sunk by a sinkhole? Well, now you do!",
+ "ND_DRLaughMessage.Collapsed": "It was like in the movies, right? You know, when the building collapses and everyone dies!",
"Alive": "Alive",
"Win": " Wins!",
"Last-": "Last ",
@@ -3983,6 +3997,8 @@
"HotPotato_Range": "Pass Range",
"HotPotato_TimeLeftSuffix": "<#00ffa5>Kill in ",
"HotPotato_HoldingNotify": "<#ffff00>You hold the Hot Potato! Pass it or die!",
+ "SurvivedTimePrefix": "Survived",
+ "SurvivedUntilTheEnd": "Until The End",
"Potato": "Player",
"PotatoInfo": "AVOID HAVING THE HOT POTATO",
"PotatoInfoLong": "AVOID HAVING THE HOT POTATO",
@@ -4023,6 +4039,34 @@
"CTF_Notify_EnemyTeamRoom": "The enemy team is in <#00ffa5>{0}",
"CTF_TeamMemberFallen": "{0} has fallen!",
"CTF_ButtonText": "PICK UP",
+ "NaturalDisasters": "NATURAL DISASTERS",
+ "ModeNaturalDisasters": "Gamemode: Natural Disasters",
+ "ModeDescribe.NaturalDisasters": "In the Natural Disasters gamemode, the map is filled with natural disasters. You have to survive them all and be the last player alive to win!\\n\\nList of all Natural Disasters and their effects:\\n- Earthquake: The ground shakes, making it hard to move.\\n- Meteor: A meteor falls from the sky, killing players in a certain range.\\n- Volcano Eruption: Lava flows from one spot, killing players.\\n- Tornado: Players are sucked into the tornado and die.\\n- Thunderstorm: Lightning strikes outside of rooms, killing players that are hit by it.\\n- Sandstorm: Sand blows, making it hard to see.\\n- Tsunami: A giant wave comes moving in a specific direction, drowning players that are in the way.\\n- Sinkhole: The ground collapses, killing players in range and any players that enter this range in the future.\\n- Building Collapse: A building on the ship collapses, killing everyone inside it. Collapsed rooms cannot be entered in the future.",
+ "ND_DisasterFrequency": "Disaster Frequency",
+ "ND_DisasterWarningTime": "Disaster Warning Time",
+ "ND_Earthquake.DurationOpt": "Earthquake Duration",
+ "ND_Earthquake.Speed": "Speed of Players Near Earthquakes",
+ "ND_VolcanoEruption.FlowStepDelay": "Delay Between Lava Flowing Steps",
+ "ND_VolcanoEruption.DurationAfterFlowComplete": "Time Before Despawning After Lava Flow is Complete",
+ "ND_Tornado.DurationOpt": "Tornado Duration",
+ "ND_Thunderstorm.DurationOpt": "Thunderstorm Duration",
+ "ND_Thunderstorm.HitFrequency": "Lightning Strike Frequency",
+ "ND_SandStorm.DurationOpt": "Sandstorm Duration",
+ "ND_SandStorm.Vision": "Vision of Players Inside Sandstorms",
+ "ND_Tsunami.MovingSpeed": "Tsunami Moving Speed",
+ "ND_Earthquake": "Earthquake",
+ "ND_Meteor": "Meteor",
+ "ND_VolcanoEruption": "Volcano Eruption",
+ "ND_Tornado": "Tornado",
+ "ND_Thunderstorm": "Thunderstorm",
+ "ND_SandStorm": "Sandstorm",
+ "ND_Tsunami": "Tsunami",
+ "ND_Sinkhole": "Sinkhole",
+ "ND_BuildingCollapse": "Building Collapse",
+ "SurvivalTime": "Survived {0}s",
+ "NDPlayer": "Natural Disasters",
+ "NDPlayerInfo": "SURVIVE ALL NATURAL DISASTERS TO WIN",
+ "NDPlayerInfoLong": "SURVIVE ALL NATURAL DISASTERS TO WIN",
"HideAndSeek": "HIDE AND SEEK",
"ModeHideAndSeek": "Gamemode: Hide And Seek",
"ModeDescribe.HideAndSeek": "In the Hide And Seek gamemode, seekers have to kill all hiders, while hiders have to complete all tasks without being killed in order to win.\\nIf the time limit reaches 0, hiders win.\\n\\nRoles: Hider, Seeker, Fox, Troll, Jumper, Detector, Jet, Dasher, Locator, Venter, Agent, Taskinator",
diff --git a/main.cs b/main.cs
index 0430ade69..837309aa4 100644
--- a/main.cs
+++ b/main.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -630,6 +631,8 @@ public class Main : BasePlugin
{ CustomRoles.Runner, "#800080" },
// Capture The Flag
{ CustomRoles.CTFPlayer, "#1313c2" },
+ // Natural Disaster
+ { CustomRoles.NDPlayer, "#03fc4a" },
// Hide And Seek
{ CustomRoles.Seeker, "#ff1919" },
{ CustomRoles.Hider, "#345eeb" },
@@ -705,7 +708,7 @@ public class Main : BasePlugin
}
}
- public void StartCoroutine(System.Collections.IEnumerator coroutine)
+ public void StartCoroutine(IEnumerator coroutine)
{
if (coroutine == null)
{
@@ -715,7 +718,7 @@ public class Main : BasePlugin
coroutines.StartCoroutine(coroutine.WrapToIl2Cpp());
}
- public void StopCoroutine(System.Collections.IEnumerator coroutine)
+ public void StopCoroutine(IEnumerator coroutine)
{
if (coroutine == null)
{