using UnityEngine; using Fusion; namespace TPSBR { public sealed class ShrinkingArea : ContextBehaviour { // PUBLIC MEMBERS public bool IsActive { get { return _flags.IsBitSet(0); } private set { _flags = _flags.SetBitNoRef(0, value); } } public bool IsShrinking { get { return _flags.IsBitSet(1); } private set { _flags = _flags.SetBitNoRef(1, value); } } public bool IsAnnounced { get { return _flags.IsBitSet(2); } private set { _flags = _flags.SetBitNoRef(2, value); } } public bool IsPaused { get { return _flags.IsBitSet(3); } private set { _flags = _flags.SetBitNoRef(3, value); } } [Networked, HideInInspector] public float ShrinkDelay { get; private set; } public TickTimer NextShrinking => _nextShrinking; public bool IsFinished => Radius <= _endRadius; [Networked, HideInInspector] public Vector3 Center { get; set; } [Networked, HideInInspector] public float Radius { get; set; } [Networked, HideInInspector] public Vector3 ShrinkCenter { get; set; } [Networked, HideInInspector] public float ShrinkRadius { get; set; } public System.Action ShrinkingAnnounced; // PRIVATE MEMBERS [Header("Size")] [SerializeField] private float _mapRadius = 200f; [SerializeField] private float _startRadius = 100f; [SerializeField] private float _endRadius = 40f; [SerializeField] private Transform _shrinkArea; [SerializeField] private Transform _shrinkAreaTarget; [Header("Timing")] [SerializeField] private float _shrinkStartDelay = 30f; [SerializeField] private float _minShrinkDelay = 35f; [SerializeField] private float _maxShrinkDelay = 90f; [SerializeField] private int _minShrinkDelayPlayers = 2; [SerializeField] private int _maxShrinkDelayPlayers = 60; [SerializeField] private float _shrinkDuration = 20f; [SerializeField] private float _shrinkAnnounceDuration = 30f; [SerializeField] private int _shrinkSteps = 5; [Header("Damage")] [SerializeField] private float _damagePerTick = 5f; [SerializeField] private float _damageTickTime = 1f; [Networked] private byte _flags { get; set; } [Networked] private TickTimer _nextShrinking { get; set; } private int _currentStage; private AreaStage[] _calculatedStages; private int _shrinkStartTick; private int _shrinkEndTick; private Material _shrinkAreaMaterial; private Material _shrinkAreaTargetMaterial; private int _materialRadiusID; private TickTimer _damageTimer; // PUBLIC METHODS public void Activate() { if (IsActive == true) return; if (HasStateAuthority == false) return; IsActive = true; if (_calculatedStages == null) { var endCenter = new Vector2(transform.position.x, transform.position.z) + Random.insideUnitCircle * (_mapRadius - _endRadius); CalculateStages(endCenter); } var startStage = _calculatedStages[0]; Center = new Vector3(startStage.Center.x, transform.position.y, startStage.Center.y); Radius = startStage.Radius; _damageTimer = TickTimer.CreateFromSeconds(Runner, _damageTickTime); SetNextShrinkingTimer(_shrinkStartDelay); } public void Deactivate() { if (IsActive == false) return; if (HasStateAuthority == false) return; IsActive = false; } public void Pause(bool pause) { if (IsPaused == pause) return; IsPaused = pause; if (pause == false) { _damageTimer = TickTimer.CreateFromSeconds(Runner, _damageTickTime); SetNextShrinkingTimer(); } } public void OverrideParameters(Vector3 center, float mapRadius, float startRadius, float endRadius) { transform.position = center; _mapRadius = mapRadius; _startRadius = startRadius; _endRadius = endRadius; } public bool SetEndCenter(Vector2 endCenter, bool force = false) { if (HasStateAuthority == false) return false; var position = new Vector2(transform.position.x, transform.position.z); if (Vector2.Distance(endCenter, position) > _mapRadius - _endRadius) { if (force == false) return false; var direction = endCenter - position; endCenter = position + direction.normalized * Random.value * (_mapRadius - _endRadius); } CalculateStages(endCenter); return true; } // NetworkBehaviour INTERFACE public override void FixedUpdateNetwork() { if (IsActive == false || IsPaused == true) return; if (HasStateAuthority == false) return; if (IsPaused == false) { if (_nextShrinking.IsRunning == true) { var remainingTime = _nextShrinking.RemainingTime(Runner); if (remainingTime <= 0f) { Shrink(); } else if (remainingTime <= _shrinkAnnounceDuration) { Announce(); } } if (IsShrinking == true) { UpdateShrinking(); } } if (_damageTimer.IsRunning == true && _damageTimer.Expired(Runner) == true) { UpdateDamage(); } } public override void Render() { if (IsActive == true) { _shrinkArea.SetActive(true); _shrinkArea.position = Center; _shrinkArea.localScale = new Vector3(Radius * 2f, 1f, Radius * 2f); _shrinkAreaMaterial.SetFloat(_materialRadiusID, Radius); if (IsAnnounced == true) { _shrinkAreaTarget.position = ShrinkCenter; _shrinkAreaTarget.localScale = new Vector3(ShrinkRadius * 2f, 1f, ShrinkRadius * 2f); _shrinkAreaTargetMaterial.SetFloat(_materialRadiusID, ShrinkRadius); _shrinkAreaTarget.SetActive(true); } else { _shrinkAreaTarget.SetActive(false); } } else { _shrinkAreaTarget.SetActive(false); _shrinkArea.SetActive(false); } } // MONOBEHAVIOUR private void Start() { _shrinkAreaMaterial = _shrinkArea.GetComponentInChildren(true).material; _shrinkAreaTargetMaterial = _shrinkAreaTarget.GetComponentInChildren(true).material; _materialRadiusID = Shader.PropertyToID("Radius"); } private void OnDrawGizmosSelected() { var tmpColor = Gizmos.color; Gizmos.color = Color.green; Gizmos.DrawWireSphere(transform.position, _startRadius); Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, _endRadius); Gizmos.color = tmpColor; } // PRIVATE METHODS private void Announce() { if (IsActive == false || IsAnnounced == true) return; if (HasStateAuthority == false) return; if (IsFinished == true) return; IsAnnounced = true; _currentStage += 1; var stage = _calculatedStages[_currentStage]; ShrinkRadius = stage.Radius; ShrinkCenter = new Vector3(stage.Center.x, transform.position.y, stage.Center.y); ShrinkingAnnounced?.Invoke(ShrinkCenter, ShrinkRadius); } private void Shrink() { if (IsActive == false || IsShrinking == true) return; if (HasStateAuthority == false) return; if (IsFinished == true) return; _shrinkStartTick = Runner.Tick; _shrinkEndTick = _shrinkStartTick + Mathf.CeilToInt(_shrinkDuration / Runner.DeltaTime); IsShrinking = true; _nextShrinking = default; } private void UpdateShrinking() { var currentTick = Runner.Tick; if (currentTick >= _shrinkEndTick) { IsAnnounced = false; IsShrinking = false; Radius = ShrinkRadius; Center = ShrinkCenter; if (_currentStage < _shrinkSteps) { SetNextShrinkingTimer(); } return; } float progress = (currentTick - _shrinkStartTick) / (float)(_shrinkEndTick - _shrinkStartTick); var previousStage = _calculatedStages[_currentStage - 1]; Radius = Mathf.Lerp(previousStage.Radius, ShrinkRadius, progress); Center = Vector3.Lerp(new Vector3(previousStage.Center.x, transform.position.y, previousStage.Center.y), ShrinkCenter, progress); } private void UpdateDamage() { var radiusSqr = Radius * Radius; foreach (var player in Context.NetworkGame.ActivePlayers) { if (player == null) continue; var agent = player.ActiveAgent; if (agent == null) continue; var agentPosition = agent.transform.position; var direction = agentPosition - Center; direction.y = 0f; if (radiusSqr > direction.sqrMagnitude) continue; var hitData = new HitData() { Action = EHitAction.Damage, HitType = EHitType.ShrinkingArea, Amount = _damagePerTick, Position = agentPosition, Target = agent.Health, Normal = Vector3.up, }; HitUtility.ProcessHit(ref hitData); } _damageTimer = TickTimer.CreateFromSeconds(Runner, _damageTickTime); } private void SetNextShrinkingTimer(float additionalTime = 0f) { int playerCount = Context.NetworkGame.ActivePlayerCount; ShrinkDelay = MathUtility.Map(_minShrinkDelayPlayers, _maxShrinkDelayPlayers, _minShrinkDelay, _maxShrinkDelay, playerCount); ShrinkDelay += additionalTime; _nextShrinking = TickTimer.CreateFromSeconds(Runner, ShrinkDelay); } private void CalculateStages(Vector2 endCenter) { _calculatedStages = new AreaStage[_shrinkSteps + 1]; Vector2 startCenter = new Vector2(transform.position.x, transform.position.z); Vector2 center = endCenter; float radius = _endRadius; // Last stage is fixed _calculatedStages[_shrinkSteps] = new AreaStage(center, radius); for (int i = _shrinkSteps - 1; i >= 0; i--) { float nextRadius = Mathf.Lerp(_startRadius, _endRadius, i / (float)_shrinkSteps); Vector2 nextCenter = Vector2.zero; bool success = false; float maxSqrDistanceFromCenter = _mapRadius - nextRadius; maxSqrDistanceFromCenter *= maxSqrDistanceFromCenter; // Just try it few times instead of complex calculation, lol for (int j = 0; j < 50; j++) { nextCenter = center + Random.insideUnitCircle * (nextRadius - radius); if (Vector2.SqrMagnitude(nextCenter - startCenter) < maxSqrDistanceFromCenter) { success = true; break; } } if (success == false) { var direction = nextCenter - startCenter; nextCenter = startCenter + direction.normalized * (_mapRadius - nextRadius); } center = nextCenter; radius = nextRadius; _calculatedStages[i] = new AreaStage(center, radius); } } // HELPERS private struct AreaStage { public Vector2 Center; public float Radius; public AreaStage(Vector2 center, float radius) { Center = center; Radius = radius; } } } }