mirror of
https://github.com/Gurge44/EndlessHostRoles.git
synced 2026-10-20 01:46:25 +00:00
340 lines
13 KiB
C#
340 lines
13 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using AmongUs.GameOptions;
|
|
using EHR.Modules;
|
|
using Hazel;
|
|
using UnityEngine;
|
|
using static EHR.Options;
|
|
|
|
namespace EHR.Roles;
|
|
|
|
internal class TimeMaster : RoleBase
|
|
{
|
|
public static bool On;
|
|
|
|
public static OptionItem TimeMasterCanRevive;
|
|
public static OptionItem TimeMasterRevivedKillerAlert;
|
|
public static OptionItem TimeMasterRevivedCanReportTheirOwnBody;
|
|
public static OptionItem TimeMasterCanUseVitals;
|
|
public static OptionItem TimeMasterRewindTimeLength;
|
|
public static OptionItem TimeMasterSkillCooldown;
|
|
public static OptionItem TimeMasterMaxUses;
|
|
public static OptionItem TimeMasterAbilityChargesWhenFinishedTasks;
|
|
public static OptionItem TimeMasterAbilityUseGainWithEachTaskCompleted;
|
|
|
|
private static Dictionary<long, Dictionary<byte, Vector2>> BackTrack = [];
|
|
private static List<byte> RevivedPlayers = [];
|
|
public static bool Rewinding;
|
|
|
|
public override bool IsEnable => On;
|
|
|
|
private bool DesyncCommsActive;
|
|
|
|
public override void SetupCustomOption()
|
|
{
|
|
SetupRoleOptions(652100, TabGroup.CrewmateRoles, CustomRoles.TimeMaster);
|
|
|
|
TimeMasterCanRevive = new BooleanOptionItem(652106, "TimeMasterCanRevive", true, TabGroup.CrewmateRoles)
|
|
.SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster]);
|
|
|
|
TimeMasterRevivedKillerAlert = new BooleanOptionItem(652107, "Altruist.ReviveTargetsKillerGetsAlert", true, TabGroup.CrewmateRoles)
|
|
.SetParent(TimeMasterCanRevive);
|
|
|
|
TimeMasterRevivedCanReportTheirOwnBody = new BooleanOptionItem(652108, "Altruist.ReviveTargetCanReportTheirOwnBody", false, TabGroup.CrewmateRoles)
|
|
.SetParent(TimeMasterCanRevive);
|
|
|
|
TimeMasterCanUseVitals = new BooleanOptionItem(652109, "TimeMasterCanUseVitals", true, TabGroup.CrewmateRoles)
|
|
.SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster]);
|
|
|
|
TimeMasterRewindTimeLength = new IntegerOptionItem(652110, "TimeMasterRewindTimeLength", new(0, 30, 1), 15, TabGroup.CrewmateRoles)
|
|
.SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster])
|
|
.SetValueFormat(OptionFormat.Seconds);
|
|
|
|
TimeMasterSkillCooldown = new FloatOptionItem(652111, "AbilityCooldown", new(0f, 180f, 1f), 20f, TabGroup.CrewmateRoles)
|
|
.SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster])
|
|
.SetValueFormat(OptionFormat.Seconds);
|
|
|
|
TimeMasterMaxUses = new IntegerOptionItem(652112, "TimeMasterMaxUses", new(0, 180, 1), 1, TabGroup.CrewmateRoles)
|
|
.SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster])
|
|
.SetValueFormat(OptionFormat.Times);
|
|
|
|
TimeMasterAbilityUseGainWithEachTaskCompleted = new FloatOptionItem(652113, "AbilityUseGainWithEachTaskCompleted", new(0f, 5f, 0.05f), 0.4f, TabGroup.CrewmateRoles)
|
|
.SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster])
|
|
.SetValueFormat(OptionFormat.Times);
|
|
|
|
TimeMasterAbilityChargesWhenFinishedTasks = new FloatOptionItem(652114, "AbilityChargesWhenFinishedTasks", new(0f, 5f, 0.05f), 0.2f, TabGroup.CrewmateRoles)
|
|
.SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster])
|
|
.SetValueFormat(OptionFormat.Times);
|
|
}
|
|
|
|
public override void Add(byte playerId)
|
|
{
|
|
On = true;
|
|
DesyncCommsActive = false;
|
|
playerId.SetAbilityUseLimit(TimeMasterMaxUses.GetFloat());
|
|
}
|
|
|
|
public override void Init()
|
|
{
|
|
On = false;
|
|
Rewinding = false;
|
|
RevivedPlayers = [];
|
|
BackTrack = [];
|
|
}
|
|
|
|
public override void ApplyGameOptions(IGameOptions opt, byte playerId)
|
|
{
|
|
if (UsePets.GetBool()) return;
|
|
|
|
AURoleOptions.EngineerCooldown = TimeMasterSkillCooldown.GetFloat();
|
|
AURoleOptions.EngineerInVentMaxTime = 1f;
|
|
}
|
|
|
|
public override void SetButtonTexts(HudManager hud, byte id)
|
|
{
|
|
if (UsePets.GetBool())
|
|
hud.PetButton.buttonLabelText.text = Translator.GetString("TimeMasterVentButtonText");
|
|
else
|
|
hud.AbilityButton.buttonLabelText.text = Translator.GetString("TimeMasterVentButtonText");
|
|
}
|
|
|
|
public static bool OnAnyoneCheckReportDeadBody(PlayerControl reporter, NetworkedPlayerInfo target)
|
|
{
|
|
if (TimeMasterRevivedCanReportTheirOwnBody.GetBool()) return true;
|
|
return !RevivedPlayers.Contains(reporter.PlayerId) || target.PlayerId != reporter.PlayerId;
|
|
}
|
|
|
|
public override void OnPet(PlayerControl pc)
|
|
{
|
|
if (Rewinding || pc.GetAbilityUseLimit() < 1) return;
|
|
pc.RpcRemoveAbilityUse(notify: false);
|
|
|
|
Main.Instance.StartCoroutine(Rewind());
|
|
|
|
if (pc.AmOwner)
|
|
Achievements.Type.APerfectTimeToRewindIt.Complete();
|
|
}
|
|
|
|
public override void OnEnterVent(PlayerControl pc, Vent vent)
|
|
{
|
|
if (UsePets.GetBool()) return;
|
|
if (Rewinding || pc.GetAbilityUseLimit() < 1) return;
|
|
pc.RpcRemoveAbilityUse(notify: false);
|
|
|
|
Main.Instance.StartCoroutine(Rewind());
|
|
|
|
if (pc.AmOwner)
|
|
Achievements.Type.APerfectTimeToRewindIt.Complete();
|
|
}
|
|
|
|
private static IEnumerator Rewind()
|
|
{
|
|
Dictionary<byte, float> originalSpeeds = Main.AllPlayerSpeed.ToDictionary(x => x.Key, x => x.Value);
|
|
|
|
try
|
|
{
|
|
Rewinding = true;
|
|
|
|
const float delay = 0.3f;
|
|
long now = Utils.TimeStamp;
|
|
int length = TimeMasterRewindTimeLength.GetInt();
|
|
float duration = length * delay + 0.55f;
|
|
|
|
Main.AllPlayerSpeed.SetAllValues(Main.MinSpeed);
|
|
ReportDeadBodyPatch.CanReport.SetAllValues(false);
|
|
|
|
string notify = Utils.ColorString(Color.yellow, string.Format(Translator.GetString("TimeMasterRewindStart"), CustomRoles.TimeMaster.ToColoredString()));
|
|
|
|
foreach (PlayerControl player in Main.EnumeratePlayerControls())
|
|
{
|
|
if (player.inVent || player.MyPhysics?.Animations?.IsPlayingEnterVentAnimation() == true) player.MyPhysics?.RpcExitVent(player.GetClosestVent().Id);
|
|
player.ReactorFlash(flashDuration: duration);
|
|
player.Notify(notify, Math.Max(duration, 4f));
|
|
player.MarkDirtySettings();
|
|
}
|
|
|
|
yield return PlayerGameOptionsSender.SendAllImmediately()?.Wait();
|
|
|
|
yield return new WaitForSecondsRealtime(0.55f);
|
|
|
|
for (long i = now - 1; i >= now - length; i--)
|
|
{
|
|
if (!BackTrack.TryGetValue(i, out Dictionary<byte, Vector2> track)) continue;
|
|
|
|
foreach ((byte playerId, Vector2 pos) in track)
|
|
{
|
|
PlayerControl player = playerId.GetPlayer();
|
|
if (!player || !player.IsAlive()) continue;
|
|
|
|
player.TP(pos);
|
|
}
|
|
|
|
yield return new WaitForSecondsRealtime(delay);
|
|
}
|
|
|
|
if (TimeMasterCanRevive.GetBool())
|
|
{
|
|
foreach (DeadBody deadBody in Object.FindObjectsOfType<DeadBody>())
|
|
{
|
|
if (!Main.PlayerStates.TryGetValue(deadBody.ParentId, out PlayerState ps)) continue;
|
|
|
|
if (ps.RealKiller.TimeStamp.AddSeconds(length) >= DateTime.Now)
|
|
{
|
|
ps.Player.RpcRevive();
|
|
ps.Player.TP(deadBody.TruePosition);
|
|
ps.Player.Notify(Translator.GetString("RevivedByTimeMaster"), 15f);
|
|
RevivedPlayers.Add(deadBody.ParentId);
|
|
}
|
|
|
|
if (TimeMasterRevivedKillerAlert.GetBool())
|
|
{
|
|
var killer = ps.RealKiller.ID.GetPlayer();
|
|
|
|
if (killer && killer.IsAlive())
|
|
{
|
|
killer.KillFlash();
|
|
killer.Notify(Translator.GetString("TimeMasterKillerAlert"), 10f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Rewinding = false;
|
|
Main.AllPlayerSpeed.AddRange(originalSpeeds);
|
|
Utils.MarkEveryoneDirtySettings();
|
|
ReportDeadBodyPatch.CanReport.SetAllValues(true);
|
|
}
|
|
}
|
|
|
|
public override void OnReportDeadBody()
|
|
{
|
|
AfterMeetingTasks();
|
|
}
|
|
|
|
public override void AfterMeetingTasks()
|
|
{
|
|
if (Rewinding)
|
|
{
|
|
Rewinding = false;
|
|
Main.AllPlayerSpeed.SetAllValues(Main.RealOptionsData.GetFloat(FloatOptionNames.PlayerSpeedMod));
|
|
ReportDeadBodyPatch.CanReport.SetAllValues(true);
|
|
}
|
|
}
|
|
|
|
public override void OnFixedUpdate(PlayerControl player)
|
|
{
|
|
if (GameStates.IsMeeting || ExileController.Instance || !player.IsAlive()) return;
|
|
|
|
long now = Utils.TimeStamp;
|
|
if (!BackTrack.TryAdd(now, null)) return;
|
|
|
|
Dictionary<byte, Vector2> targetPosition = [];
|
|
|
|
foreach (PlayerControl pc in Main.CachedAlivePlayerControls())
|
|
{
|
|
if (pc.inVent || pc.onLadder || pc.inMovingPlat) continue;
|
|
|
|
targetPosition[pc.PlayerId] = pc.Pos();
|
|
}
|
|
BackTrack[now] = targetPosition;
|
|
|
|
if (TimeMasterCanUseVitals.GetBool()) return;
|
|
|
|
var doComms = false;
|
|
bool commsSaboActive = Utils.IsActive(SystemTypes.Comms);
|
|
float usableDistance = DisableDevice.UsableDistance + 2f;
|
|
var mapId = Main.NormalOptions.MapId;
|
|
Vector2 pos = player.Pos();
|
|
|
|
if (!commsSaboActive)
|
|
{
|
|
doComms = mapId switch
|
|
{
|
|
2 => FastVector2.DistanceWithinRange(pos, DisableDevice.DevicePos["PolusVital"], usableDistance),
|
|
4 => FastVector2.DistanceWithinRange(pos, DisableDevice.DevicePos["AirshipVital"], usableDistance),
|
|
5 => FastVector2.DistanceWithinRange(pos, DisableDevice.DevicePos["FungleVital"], usableDistance),
|
|
6 when SubmergedCompatibility.IsSubmerged() => FastVector2.DistanceWithinRange(pos, DisableDevice.DevicePos["SubmergedVital"], usableDistance),
|
|
7 => DisableDevice.DevicePos.Any(kvp => kvp.Key.StartsWith("LIVital") && FastVector2.DistanceWithinRange(pos, kvp.Value, usableDistance)),
|
|
_ => false
|
|
};
|
|
}
|
|
|
|
var hasValue = false;
|
|
var activateSabComms = false;
|
|
|
|
if (doComms && !player.inVent && !DisableDevice.DesyncComms.Contains(player.PlayerId))
|
|
{
|
|
DesyncCommsActive = true;
|
|
hasValue = true;
|
|
activateSabComms = true;
|
|
}
|
|
else if (!commsSaboActive && DesyncCommsActive)
|
|
{
|
|
DesyncCommsActive = false;
|
|
hasValue = true;
|
|
}
|
|
|
|
if (!hasValue) return;
|
|
|
|
var sender = CustomRpcSender.Create("TimeMaster.DisableDevice.FixedUpdate", SendOption.Reliable, log: false);
|
|
|
|
if (activateSabComms)
|
|
sender.RpcDesyncUpdateSystem(player, SystemTypes.Comms, 128);
|
|
else
|
|
{
|
|
sender.RpcDesyncUpdateSystem(player, SystemTypes.Comms, 16);
|
|
|
|
if (mapId is 1 or 5) // Mira HQ or The Fungle
|
|
sender.RpcDesyncUpdateSystem(player, SystemTypes.Comms, 17);
|
|
}
|
|
|
|
sender.SendMessage();
|
|
}
|
|
|
|
public override bool CanUseVent(PlayerControl pc, int ventId)
|
|
{
|
|
return !IsThisRole(pc) || pc.Is(CustomRoles.Nimble) || pc.GetClosestVent()?.Id == ventId;
|
|
}
|
|
|
|
private bool KeepGameGoingCache;
|
|
private long KeepGameGoingCacheLastUpdate;
|
|
|
|
public override void ManipulateGameEndCheckCrew(PlayerState playerState, out bool keepGameGoing, out int countsAs)
|
|
{
|
|
if (playerState.IsDead)
|
|
{
|
|
base.ManipulateGameEndCheckCrew(playerState, out keepGameGoing, out countsAs);
|
|
return;
|
|
}
|
|
|
|
long now = Utils.TimeStamp;
|
|
bool cacheUpToDate = KeepGameGoingCacheLastUpdate == now;
|
|
|
|
keepGameGoing = cacheUpToDate && KeepGameGoingCache;
|
|
countsAs = 1;
|
|
|
|
if (cacheUpToDate) return;
|
|
|
|
int length = TimeMasterRewindTimeLength.GetInt();
|
|
|
|
foreach (DeadBody deadBody in Object.FindObjectsOfType<DeadBody>())
|
|
{
|
|
if (!Main.PlayerStates.TryGetValue(deadBody.ParentId, out PlayerState ps)) continue;
|
|
|
|
if (ps.RealKiller.TimeStamp.AddSeconds(length) >= DateTime.Now)
|
|
{
|
|
keepGameGoing = true;
|
|
KeepGameGoingCache = keepGameGoing;
|
|
KeepGameGoingCacheLastUpdate = now;
|
|
return;
|
|
}
|
|
}
|
|
|
|
KeepGameGoingCache = keepGameGoing;
|
|
KeepGameGoingCacheLastUpdate = now;
|
|
}
|
|
} |