using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Runtime.InteropServices; using RoR2.UI; using UnityEngine; using UnityEngine.Events; using UnityEngine.Networking; namespace RoR2; public class HoldoutZoneController : BaseZoneBehavior { public enum HoldoutZoneShape { Sphere, VerticalTube, Count } public delegate void CalcRadiusDelegate(ref float radius); public delegate void CalcChargeRateDelegate(ref float rate); public delegate void CalcAccumulatedChargeDelegate(ref float charge); public delegate void CalcColorDelegate(ref Color color); [Serializable] public class HoldoutZoneControllerChargedUnityEvent : UnityEvent { } private class ChargeHoldoutZoneObjectiveTracker : ObjectivePanelController.ObjectiveTracker { private int lastPercent = -1; private HoldoutZoneController holdoutZoneController => (HoldoutZoneController)sourceDescriptor.source; private bool ShouldBeFlashing() { bool flag = true; if ((bool)sourceDescriptor.master && (bool)holdoutZoneController) { flag = holdoutZoneController.IsBodyInChargingRadius(sourceDescriptor.master.GetBody()); } return !flag; } protected override string GenerateString() { lastPercent = holdoutZoneController.displayChargePercent; string text = string.Format(Language.GetString(holdoutZoneController.inBoundsObjectiveToken), lastPercent); if (ShouldBeFlashing()) { text = string.Format(Language.GetString(holdoutZoneController.outOfBoundsObjectiveToken), lastPercent); if ((int)(Time.time * 12f) % 2 == 0) { text = $"{text}"; } } return text; } protected override bool IsDirty() { return true; } } private class FocusConvergenceController : MonoBehaviour { private static readonly float convergenceRadiusDivisor = 2f; private static readonly float convergenceChargeRateBonus = 0.3f; private static readonly Color convergenceMaterialColor = new Color(0f, 3.9411764f, 5f, 1f); private static readonly float rampUpTime = 5f; private static readonly float startupDelay = 3f; private static readonly int cap = 3; private float currentValue; private HoldoutZoneController holdoutZoneController; private int currentFocusConvergenceCount; private Run.FixedTimeStamp enabledTime; private static readonly AnimationCurve colorCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); private void Awake() { holdoutZoneController = GetComponent(); } private void OnEnable() { enabledTime = Run.FixedTimeStamp.now; holdoutZoneController.calcRadius += ApplyRadius; holdoutZoneController.calcChargeRate += ApplyRate; holdoutZoneController.calcColor += ApplyColor; } private void OnDisable() { holdoutZoneController.calcColor -= ApplyColor; holdoutZoneController.calcChargeRate -= ApplyRate; holdoutZoneController.calcRadius -= ApplyRadius; } private void ApplyRadius(ref float radius) { if (currentFocusConvergenceCount > 0) { radius /= convergenceRadiusDivisor * (float)currentFocusConvergenceCount; } } private void ApplyColor(ref Color color) { color = Color.Lerp(color, convergenceMaterialColor, colorCurve.Evaluate(currentValue)); } private void ApplyRate(ref float rate) { if (currentFocusConvergenceCount > 0) { rate *= 1f + convergenceChargeRateBonus * (float)currentFocusConvergenceCount; } } private void Update() { DoUpdate(Time.deltaTime); } private void DoUpdate(float deltaTime) { currentFocusConvergenceCount = Util.GetItemCountForTeam(holdoutZoneController.chargingTeam, RoR2Content.Items.FocusConvergence.itemIndex, requiresAlive: true, requiresConnected: false); if (enabledTime.timeSince < startupDelay) { currentFocusConvergenceCount = 0; } currentFocusConvergenceCount = Mathf.Min(currentFocusConvergenceCount, cap); float target = (((float)currentFocusConvergenceCount > 0f) ? 1f : 0f); float num = Mathf.MoveTowards(currentValue, target, rampUpTime * deltaTime); if (currentValue <= 0f && num > 0f) { Util.PlaySound("Play_item_lunar_focusedConvergence", base.gameObject); } currentValue = num; } } public HoldoutZoneShape holdoutZoneShape; [Tooltip("The base radius of this charging sphere. Players must be within this radius to charge this zone.")] public float baseRadius; [Tooltip("No modifiers can reduce the radius below this size")] public float minimumRadius; [Tooltip("The overall change to the radius from 0% charge to 100% charge.")] public float chargeRadiusDelta; [Tooltip("How long it takes for this zone to finish charging without any modifiers.")] public float baseChargeDuration; [Tooltip("Approximately how long it should take to change from any given radius to the desired one.")] public float radiusSmoothTime; [Tooltip("An object instance which will be used to represent the clear radius.")] public Renderer radiusIndicator; [Tooltip("The child object to enable when healing nova should be active.")] public GameObject healingNovaItemEffect; public Transform healingNovaRoot; public string inBoundsObjectiveToken = "OBJECTIVE_CHARGE_TELEPORTER"; public string outOfBoundsObjectiveToken = "OBJECTIVE_CHARGE_TELEPORTER_OOB"; public bool showObjective = true; public bool applyFocusConvergence; public bool applyHealingNova = true; public bool applyDevotedEvolution; public bool applyDelusionResetChests; [Range(0f, float.MaxValue)] public float playerCountScaling = 1f; [Tooltip("If the zone is empty, this is the rate at which the charge decreases (a negative value will increase charge)")] public float dischargeRate; public HoldoutZoneControllerChargedUnityEvent onCharged; private BuffWard buffWard; private static MaterialPropertyBlock sharedColorPropertyBlock; private Color baseIndicatorColor; private float radiusVelocity; private bool wasCharged; private GameObject[] healingNovaGeneratorsByTeam = new GameObject[5]; [SyncVar] private float _charge; public float currentRadius { get; private set; } public bool isAnyoneCharging { get; private set; } public TeamIndex chargingTeam { get; set; } = TeamIndex.Player; public int displayChargePercent => Mathf.Clamp(Mathf.FloorToInt(charge * 99f), 0, 99); public float charge { get { return _charge; } private set { Network_charge = value; } } public float Network_charge { get { return _charge; } [param: In] set { SetSyncVar(value, ref _charge, 1u); } } public event CalcRadiusDelegate calcRadius; public event CalcChargeRateDelegate calcChargeRate; public event CalcAccumulatedChargeDelegate calcAccumulatedCharge; public event CalcColorDelegate calcColor; [InitDuringStartup] private static void Init() { sharedColorPropertyBlock = new MaterialPropertyBlock(); ObjectivePanelController.collectObjectiveSources += OnCollectObjectiveSources; } private void Awake() { if ((bool)radiusIndicator) { baseIndicatorColor = radiusIndicator.sharedMaterial.GetColor("_TintColor"); } buffWard = GetComponent(); } private void Start() { if (applyFocusConvergence) { base.gameObject.AddComponent(); } } private void OnEnable() { if ((bool)radiusIndicator) { radiusIndicator.enabled = true; radiusIndicator.gameObject.SetActive(value: true); } currentRadius = 0f; InstanceTracker.Add(this); } private void OnDisable() { InstanceTracker.Remove(this); currentRadius = 0f; if ((bool)radiusIndicator) { radiusIndicator.enabled = false; radiusIndicator.gameObject.SetActive(value: false); } } private void UpdateHealingNovas(bool isCharging) { if (!applyHealingNova) { return; } bool flag = false; for (TeamIndex teamIndex = TeamIndex.Neutral; teamIndex < TeamIndex.Count; teamIndex++) { bool flag2 = Util.GetItemCountForTeam(teamIndex, RoR2Content.Items.TPHealingNova.itemIndex, requiresAlive: false) > 0 && isCharging; flag = flag || flag2; if (NetworkServer.active) { ref GameObject reference = ref healingNovaGeneratorsByTeam[(int)teamIndex]; if (flag2 != (bool)reference) { if (flag2) { reference = UnityEngine.Object.Instantiate(LegacyResourcesAPI.Load("Prefabs/NetworkedObjects/TeleporterHealNovaGenerator"), healingNovaRoot ?? base.transform); reference.GetComponent().teamIndex = teamIndex; NetworkServer.Spawn(reference); } else { UnityEngine.Object.Destroy(reference); reference = null; } } } } if ((bool)healingNovaItemEffect) { healingNovaItemEffect.SetActive(flag); } } private void Update() { DoUpdate(Time.deltaTime); } private void DoUpdate(float deltaTime) { int num = CountLivingPlayers(chargingTeam); int num2 = CountPlayersInRadius(this, base.transform.position, currentRadius * currentRadius, chargingTeam); isAnyoneCharging = num2 > 0; if ((bool)Run.instance) { float radius = baseRadius + charge * chargeRadiusDelta; if (Run.instance.selectedDifficulty >= DifficultyIndex.Eclipse2) { radius *= 0.5f; } this.calcRadius?.Invoke(ref radius); currentRadius = Mathf.Max(Mathf.SmoothDamp(currentRadius, radius, ref radiusVelocity, radiusSmoothTime, float.PositiveInfinity, deltaTime), minimumRadius); } if ((bool)radiusIndicator) { float num3 = 2f * currentRadius; radiusIndicator.transform.localScale = new Vector3(num3, num3, num3); } if (NetworkServer.active && (bool)buffWard) { buffWard.Networkradius = currentRadius; } if (NetworkServer.active) { float num4 = baseChargeDuration; float rate = ((!isAnyoneCharging || num <= 0) ? (0f - dischargeRate) : (Mathf.Pow((float)num2 / (float)num, playerCountScaling) / num4)); this.calcChargeRate?.Invoke(ref rate); charge = Mathf.Clamp01(charge + rate * deltaTime); float num5 = charge; this.calcAccumulatedCharge?.Invoke(ref num5); charge = num5; } Color color = baseIndicatorColor; this.calcColor?.Invoke(ref color); sharedColorPropertyBlock.SetColor("_TintColor", color); if ((bool)radiusIndicator) { radiusIndicator.SetPropertyBlock(sharedColorPropertyBlock); } bool flag = charge >= 1f; if (wasCharged != flag) { wasCharged = flag; if (flag) { onCharged?.Invoke(this); if (applyDevotedEvolution) { UpdateDevotedLumerains(chargingTeam); } if (applyDelusionResetChests && RunArtifactManager.instance.IsArtifactEnabled(CU8Content.Artifacts.Delusion)) { foreach (ChestBehavior instances in InstanceTracker.GetInstancesList()) { instances.CallRpcResetChests(); } } } } UpdateHealingNovas(isAnyoneCharging); } private void OnDrawGizmos() { Matrix4x4 matrix = Gizmos.matrix; Color color = Gizmos.color; Gizmos.matrix = base.transform.localToWorldMatrix; Gizmos.color = new Color(0.75f, 0f, 0f, 0.5f); Gizmos.DrawWireSphere(Vector3.zero, baseRadius); Gizmos.color = color; Gizmos.matrix = matrix; } private static bool IsPointInChargingRadius(HoldoutZoneController holdoutZoneController, Vector3 origin, float chargingRadiusSqr, Vector3 point) { switch (holdoutZoneController.holdoutZoneShape) { case HoldoutZoneShape.Sphere: if ((point - origin).sqrMagnitude <= chargingRadiusSqr) { return true; } break; case HoldoutZoneShape.VerticalTube: point.y = 0f; origin.y = 0f; if ((point - origin).sqrMagnitude <= chargingRadiusSqr) { return true; } break; } return false; } private static bool IsBodyInChargingRadius(HoldoutZoneController holdoutZoneController, Vector3 origin, float chargingRadiusSqr, CharacterBody characterBody) { return IsPointInChargingRadius(holdoutZoneController, origin, chargingRadiusSqr, characterBody.corePosition); } private static int CountLivingPlayers(TeamIndex teamIndex) { int num = 0; ReadOnlyCollection teamMembers = TeamComponent.GetTeamMembers(teamIndex); for (int i = 0; i < teamMembers.Count; i++) { if (teamMembers[i].body.isPlayerControlled) { num++; } } return num; } public static int CountPlayersInRadius(HoldoutZoneController holdoutZoneController, Vector3 origin, float chargingRadiusSqr, TeamIndex teamIndex) { int num = 0; ReadOnlyCollection teamMembers = TeamComponent.GetTeamMembers(teamIndex); for (int i = 0; i < teamMembers.Count; i++) { TeamComponent teamComponent = teamMembers[i]; if (teamComponent.body.isPlayerControlled && IsBodyInChargingRadius(holdoutZoneController, origin, chargingRadiusSqr, teamComponent.body)) { num++; } } return num; } public bool IsBodyInChargingRadius(CharacterBody body) { if (!body) { return false; } return IsBodyInChargingRadius(this, base.transform.position, currentRadius * currentRadius, body); } [Server] public void FullyChargeHoldoutZone() { if (!NetworkServer.active) { Debug.LogWarning("[Server] function 'System.Void RoR2.HoldoutZoneController::FullyChargeHoldoutZone()' called on client"); } else { charge = 1f; } } private static void OnCollectObjectiveSources(CharacterMaster master, List objectiveSourcesList) { List instancesList = InstanceTracker.GetInstancesList(); int i = 0; for (int count = instancesList.Count; i < count; i++) { HoldoutZoneController holdoutZoneController = instancesList[i]; if (holdoutZoneController.showObjective && holdoutZoneController.chargingTeam == master.teamIndex) { objectiveSourcesList.Add(new ObjectivePanelController.ObjectiveSourceDescriptor { master = master, objectiveType = typeof(ChargeHoldoutZoneObjectiveTracker), source = holdoutZoneController }); } } } public override bool IsInBounds(Vector3 position) { return IsPointInChargingRadius(this, base.transform.position, currentRadius * currentRadius, position); } private void UpdateDevotedLumerains(TeamIndex teamIndex) { ReadOnlyCollection teamMembers = TeamComponent.GetTeamMembers(teamIndex); for (int i = 0; i < teamMembers.Count; i++) { TeamComponent teamComponent = teamMembers[i]; if (teamComponent.body.isPlayerControlled && (bool)teamComponent.body.master.devotionInventoryPrefab) { teamComponent.body.master.devotionInventoryPrefab.GetComponent().ActivateDevotedEvolution(); } } } private void UNetVersion() { } public override bool OnSerialize(NetworkWriter writer, bool forceAll) { bool flag = base.OnSerialize(writer, forceAll); if (forceAll) { writer.Write(_charge); return true; } bool flag2 = false; if ((base.syncVarDirtyBits & (true ? 1u : 0u)) != 0) { if (!flag2) { writer.WritePackedUInt32(base.syncVarDirtyBits); flag2 = true; } writer.Write(_charge); } if (!flag2) { writer.WritePackedUInt32(base.syncVarDirtyBits); } return flag2 || flag; } public override void OnDeserialize(NetworkReader reader, bool initialState) { base.OnDeserialize(reader, initialState); if (initialState) { _charge = reader.ReadSingle(); return; } int num = (int)reader.ReadPackedUInt32(); if (((uint)num & (true ? 1u : 0u)) != 0) { _charge = reader.ReadSingle(); } } public override void PreStartClient() { base.PreStartClient(); } }