mirror of
https://github.com/Gurge44/EndlessHostRoles.git
synced 2026-10-20 01:46:25 +00:00
485 lines
18 KiB
C#
485 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using EHR.Roles;
|
|
using Hazel;
|
|
using UnityEngine;
|
|
|
|
namespace EHR.Gamemodes;
|
|
|
|
public static class Snowdown
|
|
{
|
|
// Kill: Give the target a snowball
|
|
// Vanish: Throw snowball in movement direction / Cycle to next powerup while in shop
|
|
// Sabotage: Enter/Exit shop
|
|
// Pet: Buy the selected powerup in the shop
|
|
|
|
public static float SnowballThrowSpeed = 4f;
|
|
private static int SnowballGainFrequency = 5;
|
|
private static int MaxSnowballsInHand = 5;
|
|
private static int MaxCoinsInHand = 10;
|
|
private static bool GameEndsAfterTime = true;
|
|
private static int GameEndTime = 300;
|
|
private static bool GameEndsWhenPointsReached = true;
|
|
private static int PointsToReach = 5;
|
|
private static Dictionary<PowerUp, int> PowerUpPrices = new()
|
|
{
|
|
[PowerUp.FillUp] = 3,
|
|
[PowerUp.IncreaseCapacity] = 1,
|
|
[PowerUp.FasterPickup] = 2
|
|
};
|
|
|
|
private static OptionItem SnowballThrowSpeedOption;
|
|
private static OptionItem SnowballGainFrequencyOption;
|
|
private static OptionItem MaxSnowballsInHandOption;
|
|
private static OptionItem MaxCoinsInHandOption;
|
|
private static OptionItem GameEndsAfterTimeOption;
|
|
private static OptionItem GameEndTimeOption;
|
|
private static OptionItem GameEndsWhenPointsReachedOption;
|
|
private static OptionItem PointsToReachOption;
|
|
private static Dictionary<PowerUp, OptionItem> PowerUpPriceOptions = [];
|
|
|
|
private static readonly PowerUp[] AllPowerUp = Enum.GetValues<PowerUp>();
|
|
private static readonly StringBuilder SelfSuffix = new();
|
|
private static readonly StringBuilder Suffix = new();
|
|
private static readonly StringBuilder Statistics = new();
|
|
|
|
public static Dictionary<byte, PlayerData> Data = [];
|
|
private static List<Snowball> Snowballs = [];
|
|
public static ((float Left, float Right) X, (float Bottom, float Top) Y) MapBounds;
|
|
private static long GameStartTS;
|
|
|
|
private static readonly char[] Nums1 = "⓿❶❷❸❹❺❻❼❽❾❿".ToCharArray();
|
|
private static readonly char[] Nums2 = "⓪①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳".ToCharArray();
|
|
private static readonly char[] Nums3 = "⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇".ToCharArray();
|
|
|
|
public static void SetupCustomOption()
|
|
{
|
|
var id = 69_225_001;
|
|
Color color = Utils.GetRoleColor(CustomRoles.SnowdownPlayer);
|
|
const CustomGameMode gameMode = CustomGameMode.Snowdown;
|
|
const TabGroup tab = TabGroup.GameSettings;
|
|
|
|
SnowballThrowSpeedOption = new FloatOptionItem(id++, "Snowdown.SnowballThrowSpeedOption", new(0.25f, 10f, 0.25f), 4f, tab)
|
|
.SetHeader(true)
|
|
.SetColor(color)
|
|
.SetGameMode(gameMode)
|
|
.SetValueFormat(OptionFormat.Multiplier);
|
|
|
|
SnowballGainFrequencyOption = new IntegerOptionItem(id++, "Snowdown.SnowballGainFrequencyOption", new(1, 30, 1), 5, tab)
|
|
.SetColor(color)
|
|
.SetGameMode(gameMode)
|
|
.SetValueFormat(OptionFormat.Seconds);
|
|
|
|
MaxSnowballsInHandOption = new IntegerOptionItem(id++, "Snowdown.MaxSnowballsInHandOption", new(1, 10, 1), 5, tab)
|
|
.SetColor(color)
|
|
.SetGameMode(gameMode)
|
|
.SetValueFormat(OptionFormat.Pieces);
|
|
|
|
MaxCoinsInHandOption = new IntegerOptionItem(id++, "Snowdown.MaxCoinsInHandOption", new(1, 20, 1), 10, tab)
|
|
.SetColor(color)
|
|
.SetGameMode(gameMode)
|
|
.SetValueFormat(OptionFormat.Pieces);
|
|
|
|
GameEndsAfterTimeOption = new BooleanOptionItem(id++, "Snowdown.GameEndsAfterTimeOption", true, tab)
|
|
.SetColor(color)
|
|
.SetGameMode(gameMode);
|
|
|
|
GameEndTimeOption = new IntegerOptionItem(id++, "Snowdown.GameEndTimeOption", new(15, 1800, 15), 300, tab)
|
|
.SetColor(color)
|
|
.SetGameMode(gameMode)
|
|
.SetParent(GameEndsAfterTimeOption)
|
|
.SetValueFormat(OptionFormat.Seconds);
|
|
|
|
GameEndsWhenPointsReachedOption = new BooleanOptionItem(id++, "Snowdown.GameEndsWhenPointsReachedOption", true, tab)
|
|
.SetColor(color)
|
|
.SetGameMode(gameMode);
|
|
|
|
PointsToReachOption = new IntegerOptionItem(id++, "Snowdown.PointsToReachOption", new(1, 100, 1), 5, tab)
|
|
.SetColor(color)
|
|
.SetGameMode(gameMode)
|
|
.SetParent(GameEndsWhenPointsReachedOption)
|
|
.SetValueFormat(OptionFormat.Pieces);
|
|
|
|
PowerUpPriceOptions = AllPowerUp.ToDictionary(x => x, x => new IntegerOptionItem(id++, "Snowdown.PowerUpPriceOption", new(1, 20, 1), PowerUpPrices[x], tab)
|
|
.SetColor(color)
|
|
.SetGameMode(gameMode)
|
|
.SetValueFormat(OptionFormat.Pieces)
|
|
.AddReplacement(("{powerup}", Translator.GetString($"Snowdown.PowerUp.{x}"))));
|
|
}
|
|
|
|
public static void ApplyGameOptions()
|
|
{
|
|
AURoleOptions.PhantomCooldown = 1f;
|
|
AURoleOptions.PhantomDuration = 0.1f;
|
|
}
|
|
|
|
public static string GetSuffix(PlayerControl seer, PlayerControl target)
|
|
{
|
|
if (!Data.TryGetValue(seer.PlayerId, out PlayerData seerData) || !Data.TryGetValue(target.PlayerId, out PlayerData targetData)) return string.Empty;
|
|
|
|
if (seer.PlayerId == target.PlayerId)
|
|
return seerData.GetSelfSuffix();
|
|
|
|
Suffix.Clear().Append("<#ffffff>");
|
|
AddStats(Suffix, targetData);
|
|
Suffix.Append("</color>");
|
|
return Suffix.ToString();
|
|
}
|
|
|
|
private static void AddStats(StringBuilder sb, PlayerData targetData)
|
|
{
|
|
sb.Append("<#e4fdff>")
|
|
.Append(Nums1[targetData.SnowballsReady])
|
|
.Append("</color>")
|
|
.Append(' ')
|
|
.Append("<#dfc57b>")
|
|
.Append(Nums2[targetData.Coins])
|
|
.Append("</color>");
|
|
|
|
if (targetData.Points > 0)
|
|
{
|
|
sb.Append(' ')
|
|
.Append("<#e5acff>")
|
|
.Append(Nums3[targetData.Points - 1])
|
|
.Append("</color>");
|
|
}
|
|
}
|
|
|
|
public static string GetStatistics(byte id)
|
|
{
|
|
if (!Data.TryGetValue(id, out PlayerData data)) return string.Empty;
|
|
Statistics.Clear().Append("<#ffffff>");
|
|
AddStats(Statistics, data);
|
|
Statistics.Append("</color>");
|
|
return Statistics.ToString();
|
|
}
|
|
|
|
public static string GetHudText()
|
|
{
|
|
if (!GameEndsAfterTime) return string.Empty;
|
|
long timeLeft = GameEndTime - (Utils.TimeStamp - GameStartTS);
|
|
return $"{timeLeft / 60:00}:{timeLeft % 60:00}";
|
|
}
|
|
|
|
public static bool CheckGameEnd(out GameOverReason reason)
|
|
{
|
|
reason = GameOverReason.ImpostorsByKill;
|
|
if (GameStates.IsEnded || !Main.IntroDestroyed) return false;
|
|
var aapc = Main.CachedAlivePlayerControls();
|
|
|
|
switch (aapc.Count)
|
|
{
|
|
case 1:
|
|
{
|
|
PlayerControl winner = aapc[0];
|
|
Logger.Info($"Winner: {winner.GetRealName().RemoveHtmlTags()}", "Snowdown");
|
|
CustomWinnerHolder.WinnerIds = [winner.PlayerId];
|
|
Main.DoBlockNameChange = true;
|
|
return true;
|
|
}
|
|
case 0:
|
|
{
|
|
CustomWinnerHolder.ResetAndSetWinner(CustomWinner.None);
|
|
Logger.Warn("No players alive. Force ending the game", "Snowdown");
|
|
return true;
|
|
}
|
|
default:
|
|
{
|
|
if (GameEndsAfterTime && Utils.TimeStamp - GameStartTS >= GameEndTime)
|
|
{
|
|
int max = Data.Values.Max(x => x.Points);
|
|
CustomWinnerHolder.WinnerIds = Data.Where(x => x.Value.Points == max && x.Key.GetPlayer()).Select(x => x.Key).ToHashSet();
|
|
Logger.Info($"Winners: {string.Join(", ", CustomWinnerHolder.WinnerIds.Select(x => Main.AllPlayerNames.GetValueOrDefault(x, "[Unknown player]")))}", "Snowdown");
|
|
Main.DoBlockNameChange = true;
|
|
return true;
|
|
}
|
|
|
|
if (GameEndsWhenPointsReached && Data.IntersectBy(Main.EnumerateAlivePlayerControls().Select(p => p.PlayerId), x => x.Key).FindFirst(x => x.Value.Points >= PointsToReach, out var winnerData))
|
|
{
|
|
CustomWinnerHolder.WinnerIds = [winnerData.Key];
|
|
Logger.Info($"Winners: {Main.AllPlayerNames.GetValueOrDefault(winnerData.Key, "[Unknown player]")}", "Snowdown");
|
|
Main.DoBlockNameChange = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void Init()
|
|
{
|
|
SnowballThrowSpeed = SnowballThrowSpeedOption.GetFloat();
|
|
SnowballGainFrequency = SnowballGainFrequencyOption.GetInt();
|
|
MaxSnowballsInHand = MaxSnowballsInHandOption.GetInt();
|
|
MaxCoinsInHand = MaxCoinsInHandOption.GetInt();
|
|
GameEndsAfterTime = GameEndsAfterTimeOption.GetBool();
|
|
GameEndTime = GameEndTimeOption.GetInt();
|
|
GameEndsWhenPointsReached = GameEndsWhenPointsReachedOption.GetBool();
|
|
PointsToReach = PointsToReachOption.GetInt();
|
|
PowerUpPrices = PowerUpPriceOptions.ToDictionary(x => x.Key, x => x.Value.GetInt());
|
|
|
|
Data = Main.EnumeratePlayerControls().ToDictionary(x => x.PlayerId, _ => new PlayerData());
|
|
Snowballs = [];
|
|
|
|
List<Vector2> rooms = Main.LIMap
|
|
? ShipStatus.Instance.AllRooms.Select(x => new Vector2(x.transform.position.x, x.transform.position.y)).ToList()
|
|
: RandomSpawn.SpawnMap.GetSpawnMap().Positions?.Values.ToList();
|
|
|
|
if (rooms == null) return;
|
|
|
|
List<float> x = rooms.ConvertAll(r => r.x);
|
|
List<float> y = rooms.ConvertAll(r => r.y);
|
|
|
|
const float extend = 5f;
|
|
MapBounds = ((x.Min() - extend, x.Max() + extend), (y.Min() - extend, y.Max() + extend));
|
|
}
|
|
|
|
public static void GameStart() // Called as non-host client too!
|
|
{
|
|
GameStartTS = Utils.TimeStamp;
|
|
if (!AmongUsClient.Instance.AmHost) GameEndTime = GameEndTimeOption.GetInt();
|
|
}
|
|
|
|
public static void OnCheckMurder(PlayerControl killer, PlayerControl target)
|
|
{
|
|
killer.SetKillCooldownNonSync(5f);
|
|
if (!Data.TryGetValue(killer.PlayerId, out PlayerData killerData) || !Data.TryGetValue(target.PlayerId, out PlayerData targetData) || killerData.SnowballsReady < 1 || targetData.SnowballsReady >= targetData.MaxSnowballsReady) return;
|
|
killerData.SnowballsReady--;
|
|
targetData.SnowballsReady++;
|
|
}
|
|
|
|
//[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))]
|
|
public static class FixedUpdatePatch
|
|
{
|
|
public static void Postfix(PlayerControl __instance)
|
|
{
|
|
if (!AmongUsClient.Instance.AmHost || !GameStates.IsInTask || ExileController.Instance || AntiBlackout.SkipTasks || Options.CurrentGameMode != CustomGameMode.Snowdown || !Main.IntroDestroyed) return;
|
|
if (!__instance.IsAlive() || !Data.TryGetValue(__instance.PlayerId, out PlayerData data)) return;
|
|
|
|
long now = Utils.TimeStamp;
|
|
Vector2 pos = __instance.Pos();
|
|
Snowball touchingSnowball = Snowballs.Find(x => x.Active && x.Thrower != __instance && FastVector2.DistanceWithinRange(x.Position, pos, 1.5f));
|
|
|
|
if (touchingSnowball != null)
|
|
{
|
|
touchingSnowball.SetInactive();
|
|
|
|
if (touchingSnowball.Thrower && Data.TryGetValue(touchingSnowball.Thrower.PlayerId, out PlayerData throwerData) && throwerData.Coins < throwerData.MaxCoins)
|
|
throwerData.Coins++;
|
|
}
|
|
|
|
if (data.SnowballsReady < data.MaxSnowballsReady && now - data.LastSnowballGainTS >= data.SnowballGainInterval)
|
|
{
|
|
data.SnowballsReady++;
|
|
data.LastSnowballGainTS = now;
|
|
}
|
|
|
|
if (!FastVector2.DistanceWithinRange(pos, data.LastPosition, 0.01f))
|
|
{
|
|
data.LastLastPosition = data.LastPosition;
|
|
data.LastPosition = pos;
|
|
}
|
|
|
|
Utils.NotifyRoles(SpecifySeer: __instance, SendOption: SendOption.None);
|
|
}
|
|
}
|
|
|
|
public class PlayerData
|
|
{
|
|
public Vector2 LastPosition = new(0f, 0f);
|
|
public Vector2 LastLastPosition = new(0f, 0f);
|
|
public int SnowballsReady;
|
|
public int MaxSnowballsReady = MaxSnowballsInHand;
|
|
public long LastSnowballGainTS;
|
|
public int SnowballGainInterval = SnowballGainFrequency;
|
|
public int Coins;
|
|
public int MaxCoins = MaxCoinsInHand;
|
|
public bool InShop;
|
|
private int ShopSelectedIndex;
|
|
public int Points;
|
|
private bool ShowHelp;
|
|
|
|
public void OnSabotage(PlayerControl pc)
|
|
{
|
|
InShop = (SnowballsReady != MaxSnowballsReady || MaxSnowballsReady < 20 || MaxCoins < 10 || SnowballGainInterval > 1) && !InShop;
|
|
ShowHelp = !Main.HasPlayedGM[CustomGameMode.Snowdown].Contains(pc.FriendCode);
|
|
if (pc.AmOwner) HudSpritePatch.ForceUpdate = true;
|
|
}
|
|
|
|
public void OnPet(PlayerControl pc)
|
|
{
|
|
if (!InShop)
|
|
{
|
|
if (Coins >= 20)
|
|
{
|
|
Points++;
|
|
Coins -= 20;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
PowerUp powerUp = AllPowerUp[ShopSelectedIndex];
|
|
int cost = PowerUpPrices[powerUp];
|
|
if (Coins < cost) return;
|
|
|
|
switch (powerUp)
|
|
{
|
|
case PowerUp.FillUp:
|
|
{
|
|
if (SnowballsReady == MaxSnowballsReady) return;
|
|
SnowballsReady = MaxSnowballsReady;
|
|
break;
|
|
}
|
|
case PowerUp.IncreaseCapacity:
|
|
{
|
|
switch (MaxSnowballsReady, MaxCoins)
|
|
{
|
|
case (>= 10, >= 20):
|
|
return;
|
|
case (>= 10, < 20):
|
|
MaxCoins++;
|
|
break;
|
|
case (< 10, >= 20):
|
|
MaxSnowballsReady++;
|
|
break;
|
|
case (< 10, < 20):
|
|
MaxSnowballsReady++;
|
|
MaxCoins++;
|
|
break;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case PowerUp.FasterPickup:
|
|
{
|
|
if (SnowballGainInterval <= 1) return;
|
|
SnowballGainInterval--;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Coins -= cost;
|
|
|
|
if (SnowballsReady == MaxSnowballsReady && MaxSnowballsReady >= 20 && MaxCoins >= 10 && SnowballGainInterval <= 1)
|
|
InShop = false;
|
|
}
|
|
|
|
public void OnVanish(PlayerControl pc)
|
|
{
|
|
if (InShop)
|
|
{
|
|
ShopSelectedIndex++;
|
|
|
|
if (ShopSelectedIndex >= AllPowerUp.Length)
|
|
ShopSelectedIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
if (SnowballsReady <= 0) return;
|
|
SnowballsReady--;
|
|
|
|
Vector2 from = pc.Pos();
|
|
Vector2 direction = (LastPosition - LastLastPosition).normalized;
|
|
|
|
Snowball inactive = Snowballs.Find(x => !x.Active);
|
|
|
|
if (inactive != null) inactive.Reuse(from, direction, pc);
|
|
else Snowballs.Add(new Snowball(from, direction, pc));
|
|
}
|
|
}
|
|
|
|
public string GetSelfSuffix()
|
|
{
|
|
SelfSuffix.Clear().Append("<#ffffff><size=140%>");
|
|
|
|
AddStats(SelfSuffix, this);
|
|
|
|
SelfSuffix.Append("</size>");
|
|
|
|
if (InShop)
|
|
{
|
|
PowerUp powerUp = AllPowerUp[ShopSelectedIndex];
|
|
SelfSuffix.Append('\n')
|
|
.Append('\n')
|
|
.Append(Translator.GetString($"Snowdown.PowerUp.{powerUp}"))
|
|
.Append(' ')
|
|
.Append("<#dfc57b>")
|
|
.Append(Nums2[PowerUpPrices[powerUp]])
|
|
.Append("</color>")
|
|
.Append('\n')
|
|
.Append("<size=90%>")
|
|
.Append(Translator.GetString($"Snowdown.PowerUp.{powerUp}.Description"))
|
|
.Append("</size>");
|
|
|
|
if (ShowHelp)
|
|
{
|
|
SelfSuffix.Append('\n')
|
|
.Append('\n')
|
|
.Append("<size=80%>")
|
|
.Append(Translator.GetString("Snowdown.ShopHelp"))
|
|
.Append("</size>");
|
|
}
|
|
}
|
|
|
|
SelfSuffix.Append("</color>");
|
|
return SelfSuffix.ToString();
|
|
}
|
|
}
|
|
|
|
public enum PowerUp
|
|
{
|
|
FillUp,
|
|
IncreaseCapacity,
|
|
FasterPickup
|
|
}
|
|
}
|
|
|
|
internal class SnowdownPlayer : RoleBase
|
|
{
|
|
public static bool On;
|
|
|
|
public override bool IsEnable => On;
|
|
|
|
public override void SetupCustomOption() { }
|
|
|
|
public override void Init()
|
|
{
|
|
On = false;
|
|
}
|
|
|
|
public override void Add(byte playerId)
|
|
{
|
|
On = true;
|
|
}
|
|
|
|
public override bool CanUseSabotage(PlayerControl pc)
|
|
{
|
|
return pc.IsAlive();
|
|
}
|
|
|
|
public override bool OnSabotage(PlayerControl pc)
|
|
{
|
|
if (Snowdown.Data.TryGetValue(pc.PlayerId, out Snowdown.PlayerData data))
|
|
data.OnSabotage(pc);
|
|
|
|
return false;
|
|
}
|
|
|
|
public override bool OnVanish(PlayerControl pc)
|
|
{
|
|
if (Snowdown.Data.TryGetValue(pc.PlayerId, out Snowdown.PlayerData data))
|
|
data.OnVanish(pc);
|
|
|
|
return false;
|
|
}
|
|
|
|
public override void OnPet(PlayerControl pc)
|
|
{
|
|
if (Snowdown.Data.TryGetValue(pc.PlayerId, out Snowdown.PlayerData data))
|
|
data.OnPet(pc);
|
|
}
|
|
}
|