using System.Collections.Generic; using ProjectI.Player; using ProjectI.Runs; using Unity.Netcode; using UnityEngine; namespace ProjectI.Combat { public sealed class PrototypePlayerVitals : NetworkBehaviour { private static readonly List activePlayers = new List(); private readonly NetworkVariable health = new NetworkVariable(50); private readonly NetworkVariable maximum = new NetworkVariable(50); private readonly NetworkVariable shield = new NetworkVariable(50); private readonly NetworkVariable maximumShield = new NetworkVariable(50); private readonly NetworkVariable overshield = new NetworkVariable(); private readonly NetworkVariable overshieldCap = new NetworkVariable(50); private readonly NetworkVariable rechargeStartsAt = new NetworkVariable(); private readonly NetworkVariable reviveProgress = new NetworkVariable(); private NetworkList overshieldSources; private readonly List sourceSnapshots=new List(); private readonly ReviveTracker reviveTracker = new ReviveTracker(); private PlayerDefenseState state; private PrototypeFirstPersonPlayer movement; private PrototypePlayerCombat combat; private double lastReviveInput = double.NegativeInfinity; private bool reviveHeld; private uint lastReviveSequence; private readonly ShieldFamilyBudget sharedShelterBudget=new ShieldFamilyBudget(); private readonly ShieldFamilyBudget wardBudget=new ShieldFamilyBudget(); public static IReadOnlyList ActivePlayers => activePlayers; public float CurrentHealth => health.Value; public float MaximumHealth => maximum.Value; public float Shield => shield.Value; public float MaximumShield => maximumShield.Value; public float Overshield => overshield.Value; public float OvershieldCap => overshieldCap.Value; public IEnumerable ActiveOvershieldSources { get { if(overshieldSources==null)yield break; for(int i=0;i rechargeStartsAt.Value; public bool IsDowned => health.Value <= 0; public bool IsAlive => !IsDowned; public float LastDamageTakenTime { get; private set; } = float.NegativeInfinity; public float ReviveProgress => reviveProgress.Value; public string DisplayName => OwnerClientId == Unity.Netcode.NetworkManager.ServerClientId ? "Host" : "Player " + OwnerClientId; public Vector3 AcceptedPosition => movement.ServerAcceptedPosition; public bool SpatiallySettled => movement != null && movement.SpatiallySettled; private void Awake() { overshieldSources = new NetworkList(); state = new PlayerDefenseState(); movement = GetComponent(); combat = GetComponent(); } public override void OnNetworkSpawn() { sharedShelterBudget.Clear();wardBudget.Clear(); LastDamageTakenTime = float.NegativeInfinity; activePlayers.Add(this); if (IsServer) SyncState(); } public override void OnNetworkDespawn() { activePlayers.Remove(this); reviveTracker.Reset(); } public override void OnDestroy() { activePlayers.Remove(this); overshieldSources?.Dispose(); base.OnDestroy(); } private void Update() { if (!IsSpawned || !IsServer) return; double now = NetworkManager.ServerTime.Time; state.Tick(now, CanRechargeShield); SyncState(); UpdateRevive(now); } public void SetMaximumServer(float value, bool restoreFullHealth = false) { if (!IsServer || !IsSpawned) return; if (restoreFullHealth) state.Reset(value, NetworkManager.ServerTime.Time); else state.ChangeMaximum(value); SyncState(); } public float DamageServer(float amount, string cause = "Damage") { if (!IsServer || !IsSpawned || ProjectI.Runs.PrototypeRunDirector.BlockCombat) return 0; if (ProjectI.Debugging.DebugScenarioController.ServerInvulnerability) return 0; bool wasAlive = !state.IsDowned; float previousHealth = state.Current; var effects=ProjectI.Runs.PrototypeRunDirector.Instance?.Effects; float applied = state.ApplyDamage(amount, NetworkManager.ServerTime.Time,effects?.ShieldRechargeDelayMultiplier(OwnerClientId)??1); if(applied>0)effects?.Damaged(OwnerClientId,previousHealth,state.Current,state.Maximum,NetworkManager.ServerTime.Time); ProjectI.Runs.PrototypeRunDirector.Instance?.RecordPlayerDamage(OwnerClientId, applied, cause); if (wasAlive && state.IsDowned) { sharedShelterBudget.Clear();wardBudget.Clear(); ProjectI.Runs.PrototypeRunDirector.Instance?.RecordPlayerDown(OwnerClientId); combat.ClearTemporaryEffectsServer(); reviveHeld = false; reviveTracker.Reset(); reviveProgress.Value = 0; } SyncState(); if (wasAlive && state.IsDowned) movement.ReanchorPendingDownedServer(); if (applied > 0) DamageTakenRpc(); if (!state.IsDowned && state.Current < previousHealth) GetComponent()?.NotifyHealthDamaged(previousHealth - state.Current); return applied; } [Rpc(SendTo.Owner, InvokePermission = RpcInvokePermission.Server)] private void DamageTakenRpc() { if (IsOwner) LastDamageTakenTime = Time.unscaledTime; } public float HealServer(float amount) { if (!IsServer || !IsSpawned) return 0; float healed = state.Heal(amount); ProjectI.Runs.PrototypeRunDirector.Instance?.RecordPlayerHealing(OwnerClientId, healed); if (healed > 0) SyncState(); return healed; } public void ChangeRunMaximumServer(float value) { if (IsServer && IsSpawned) { state.ChangeMaximumPreservingMissing(value); SyncState(); } } public void ChangeOvershieldCapacityServer(float capacity) { if (IsServer && IsSpawned) { state.ChangeShieldMaximum(state.MaximumShield, capacity); SyncState(); } } public void ChangeNormalShieldMaximumServer(float capacity) {if(IsServer&&IsSpawned){state.ChangeNormalShieldMaximumPreservingMissing(capacity);SyncState();}} public void RemoveOvershieldSourceServer(EffectSourceKey source) {if(IsServer&&IsSpawned){state.RemoveOvershieldSource(source);SyncState();}} public void RemoveOvershieldOwnerServer(ulong owner) { if(!IsServer||!IsSpawned)return; state.CopyActiveOvershieldSources(sourceSnapshots); foreach(var source in sourceSnapshots)if(source.Source.SourceOwner==owner)state.RemoveOvershieldSource(source.Source); SyncState(); } public void CopyActiveBuffs(List destination,double now) { foreach(var source in ActiveOvershieldSources) if(source.Amount>0&&nowGrantSharedShelterFamilyServer(amount,owner,duration,UpgradeEffectResolver.Resolve(UpgradeId.SharedShelter,1).TriggerInterval); public void GrantSharedShelterFamilyServer(float amount,ulong owner,double duration,double familyInterval) { if(!IsServer||!IsSpawned||!IsAlive)return; float granted=sharedShelterBudget.Request(amount,NetworkManager.ServerTime.Time,familyInterval); if(granted>0)GrantShieldServer(granted,true,EffectSourceKey.Core(UpgradeId.SharedShelter,owner),duration); } public void GrantWardFamilyServer(float amount) =>GrantWardFamilyServer(amount,ProjectI.Builds.AbilityEffectParameters.Defaults(ProjectI.Builds.AbilityEffect.WardBeacon).TickSeconds); public void GrantWardFamilyServer(float amount,double familyInterval) { if(!IsServer||!IsSpawned||!IsAlive)return; float granted=wardBudget.Request(amount,NetworkManager.ServerTime.Time,familyInterval); if(granted>0)GrantShieldServer(granted); } public ShieldGrantResult GrantShieldServer(float amount, bool allowOvershield = false, EffectSourceKey sourceId = default, double overshieldDuration = 0) { if (!IsServer || !IsSpawned || state.IsDowned) return default; double now = NetworkManager.ServerTime.Time; state.Tick(now, CanRechargeShield); var result = state.GrantShield(amount, now, allowOvershield, sourceId, overshieldDuration); SyncState(); return result; } public void AddBarrierServer(float amount, double duration, EffectSourceKey source = default) => GrantShieldServer(amount, true, source, duration); public void ResetServer() { sharedShelterBudget.Clear();wardBudget.Clear(); if (!IsServer || !IsSpawned) return; state.Reset(state.Maximum, NetworkManager.ServerTime.Time); reviveHeld = false; lastReviveInput = double.NegativeInfinity; reviveTracker.Reset(); reviveProgress.Value = 0; SyncState(); } public void RequestRevive(bool holding, uint sequence) { if (IsOwner && IsSpawned) ReviveInputRpc(holding, sequence); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] private void ReviveInputRpc(bool holding, uint sequence, RpcParams rpc = default) { if (rpc.Receive.SenderClientId != OwnerClientId || sequence == 0 || sequence <= lastReviveSequence) return; lastReviveSequence = sequence; reviveHeld = holding && !state.IsDowned && !ProjectI.Runs.PrototypeRunDirector.BlockCombat; reviveTracker.SetHolding(reviveHeld); if (!reviveHeld) reviveProgress.Value = 0; lastReviveInput = NetworkManager.ServerTime.Time; } private void UpdateRevive(double now) { PrototypePlayerVitals target = null; float nearest = CombatAuthorityRules.ReviveRange * CombatAuthorityRules.ReviveRange; bool validInput = reviveHeld && !state.IsDowned && SpatiallySettled && !ProjectI.Runs.PrototypeRunDirector.BlockCombat && CombatAuthorityRules.IsReviveInputFresh(now, lastReviveInput); if (validInput) { foreach (var candidate in activePlayers) { if (!candidate.SpatiallySettled) continue; if (!CombatAuthorityRules.CanRevive(OwnerClientId, candidate.OwnerClientId, true, candidate.IsDowned, AcceptedPosition, candidate.AcceptedPosition)) continue; float distance = (AcceptedPosition - candidate.AcceptedPosition).sqrMagnitude; if (distance > nearest) continue; if (Physics.Linecast(AcceptedPosition + Vector3.up, candidate.AcceptedPosition + Vector3.up, ~LayerMask.GetMask("Players"), QueryTriggerInteraction.Ignore)) continue; nearest = distance; target = candidate; } } bool complete = reviveTracker.Tick(target != null ? target.NetworkObjectId : 0, target != null, now,ProjectI.Runs.PrototypeRunDirector.Instance?.Effects?.ReviveRateMultiplier(OwnerClientId)??1); float progress = Mathf.Round(reviveTracker.Progress * 20) / 20; if (!Mathf.Approximately(progress, reviveProgress.Value)) reviveProgress.Value = progress; if (complete && target != null && target.state.TryRevive(0.3f, now)) { ProjectI.Runs.PrototypeRunDirector.Instance?.RecordPlayerRevive(OwnerClientId); ProjectI.Runs.PrototypeRunDirector.Instance?.Effects?.Revived(OwnerClientId,target.OwnerClientId,now); target.SyncState(); reviveTracker.Reset(); reviveProgress.Value = 0; } } private bool CanRechargeShield => ProjectI.Runs.PrototypeRunDirector.ShieldRechargeAllowed; private void SyncState() { health.Value = state.Current; maximum.Value = state.Maximum; shield.Value = state.Shield; maximumShield.Value = state.MaximumShield; overshield.Value = state.Overshield; overshieldCap.Value = state.OvershieldCap; rechargeStartsAt.Value = state.RechargeStartsAt; state.CopyActiveOvershieldSources(sourceSnapshots); // Stable source order lets unchanged frames emit no list changes and allocate no snapshots. for(int i=0;i=overshieldSources.Count)overshieldSources.Add(sourceSnapshots[i]); else if(!overshieldSources[i].Equals(sourceSnapshots[i]))overshieldSources[i]=sourceSnapshots[i]; } while(overshieldSources.Count>sourceSnapshots.Count)overshieldSources.RemoveAt(overshieldSources.Count-1); } } }