Files
EndlessHostRoles/Roles/Standard/Crewmate/Support/TimeMaster.cs
T
Newchan 21d403ac04 Add Device Positions for LevelImpostor Maps (Experimental) (#680)
* add devices estimation for LI

* final touches
2026-08-23 09:44:10 +02:00

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;
}
}