using System.Collections.Generic; using ProjectI.Builds; using ProjectI.Player; using ProjectI.Sessions; using ProjectI.Runs; using ProjectI.Settings; using ProjectI.Enemies; using ProjectI.Combat.Statuses; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; namespace ProjectI.Combat { public sealed class PrototypePlayerCombat : NetworkBehaviour { [SerializeField] private WeaponDefinition firstWeapon; [SerializeField] private WeaponDefinition secondWeapon; [SerializeField] private AbilityDefinition[] abilityDefinitions; private readonly NetworkVariable weapon = new NetworkVariable(); private readonly NetworkVariable ability = new NetworkVariable(); private readonly NetworkVariable loadout = new NetworkVariable(BuildLoadout.Default); private readonly NetworkVariable stats = new NetworkVariable(new BuildStats(50, 5, 1)); private readonly Dictionary definitions = new Dictionary(); private readonly Dictionary catalog = new Dictionary(); private readonly Collider[] areaHits = new Collider[64]; private readonly HashSet areaTargets = new HashSet(); private WeaponStateMachine weaponState; private PlayerBuildState buildState; private AbilityStateMachine abilityState; private PrototypeFirstPersonPlayer movement; private PrototypeWeaponPresentation presentation; private PrototypeArenaState arena; private PrototypePlayerVitals vitals; private PrototypePlayerPassiveEffects passives; private uint observedReloadCompletions; private uint weaponSequence, abilitySequence, reviveSequence; private double nextLocalShot; private double nextReviveInput; private bool previouslyHoldingRevive; private float localDashUntil; private Vector3 localDashVelocity; private double serverDashUntil; private double serverDashMotionUntil; private uint nextServerDashSequence, serverDashSequence; private float serverDashSpeed; private bool serverSlipstream; private float serverSlipstreamTravel; private float serverSlipstreamBudget; private readonly NetworkVariable confirmedFocusTriggers=new NetworkVariable(); private NetworkList activeBuffs; private readonly List buffScratch=new List(32); private readonly NetworkVariable confirmedRailTriggers=new NetworkVariable(); private readonly NetworkVariable confirmedFields=new NetworkVariable(); private readonly NetworkVariable confirmedProjectiles=new NetworkVariable(); private readonly NetworkVariable confirmedStatusDurationBonus=new NetworkVariable(); private readonly NetworkVariable firstWeaponId = new NetworkVariable(1); private readonly NetworkVariable secondWeaponId = new NetworkVariable(2); private readonly NetworkVariable runFireRate = new NetworkVariable(1); private readonly NetworkVariable runReloadRate = new NetworkVariable(1); private readonly NetworkVariable runRechargeRate = new NetworkVariable(1); private readonly NetworkVariable runRecoilRecovery = new NetworkVariable(1); private readonly NetworkVariable rhythmUntil = new NetworkVariable(); private readonly NetworkVariable overdriveUntil = new NetworkVariable(); private uint heldSequence, lastHeldSequence; private bool locallyHoldingFire, serverHoldingFire; #if UNITY_EDITOR || DEVELOPMENT_BUILD private bool arrivalFireWasHeld; #endif private double nextHeldInput, lastHeldInput, nextHeldShot; private Vector3 heldAim; private double? scheduledEventTime; public double EventTimeServer => IsServer && scheduledEventTime.HasValue ? scheduledEventTime.Value : Now; private readonly NetworkVariable runWeakPointBonus = new NetworkVariable(); private readonly NetworkVariable runSpreadMultiplier = new NetworkVariable(1); private readonly NetworkVariable runMagazineBonus = new NetworkVariable(); private static ulong nextAttackId; private static uint nextSpawnLifetime; private readonly NetworkVariable spawnLifetime=new NetworkVariable(); public uint SpawnLifetime=>spawnLifetime.Value; public AbilityExpansionRuntime Expansion {get;private set;} private Vector3 preparedExpansionPoint; private AbilityCastRecord currentCast; private float currentCastMultiplier=1; private double movementAbilityEnds; private Vector3 movementAbilityStart; private float movementAbilityTravel; public GunTriggerBonus CurrentGunBonus {get;private set;} public int FocusTriggersRemaining=>confirmedFocusTriggers.Value; public int RailTriggersRemaining=>confirmedRailTriggers.Value; public int ActiveAbilityFields=>confirmedFields.Value; public int PendingAbilityProjectiles=>confirmedProjectiles.Value; public int DebugPendingActionCount => IsServer ? (Expansion?.FieldCount ?? 0) + (Expansion?.PendingCount ?? 0) + starfallRemaining + (serverHoldingFire ? 1 : 0) : confirmedFields.Value + confirmedProjectiles.Value; public int LocalAbilityCueCount=>expansionVisuals.Count; public ulong CurrentAttackId { get; private set; } public static PrototypePlayerCombat LocalPlayer { get; private set; } public PrototypePlayerVitals Vitals => vitals; public PrototypePlayerPassiveEffects Passives => passives; public float AttackDamageMultiplier => Stats.AttackDamageMultiplier * (passives != null ? passives.AttackMultiplier : 1); public float AbilityDamageMultiplier => Stats.AbilityDamageMultiplier*currentCastMultiplier; public float PassiveStatusDurationBonus => (Loadout.PassiveFirst==7||Loadout.PassiveSecond==7) &&definitions.TryGetValue(7,out var definition)&&definition is TacticianAbilityDefinition tactician?tactician.statusDurationBonus:0; public float StatusDurationBonus => IsServer?PassiveStatusDurationBonus+(PrototypeRunDirector.Instance?.Effects?.StatusDurationBonus(OwnerClientId)??0):confirmedStatusDurationBonus.Value; public Camera PlayerCamera => movement.PlayerCamera; public WeaponDefinition ActiveWeapon => WeaponRoster.Get(ActiveSlot == 0 ? firstWeaponId.Value : secondWeaponId.Value); public WeaponDefinition FirstWeapon => WeaponRoster.Get(firstWeaponId.Value); public WeaponDefinition SecondWeapon => WeaponRoster.Get(secondWeaponId.Value); public float WeakPointBonus => runWeakPointBonus.Value; public float SpreadMultiplier => runSpreadMultiplier.Value; public bool IsFinalRoundServer => IsServer && weaponState != null && (weaponState.ActiveSlot == 0 ? weaponState.AmmoFirst : weaponState.AmmoSecond) == 0; public long UpgradeRank(UpgradeId id) => PrototypeRunDirector.Instance?.Inventory(OwnerClientId)?.Rank(id) ?? 0; public int ActiveSlot => weapon.Value.ActiveSlot; public long Ammo => ActiveSlot == 0 ? weapon.Value.AmmoFirst : weapon.Value.AmmoSecond; public long MagazineCapacity => MagazineCapacityForSlot(ActiveSlot); public long MagazineCapacityForSlot(int slot) => UpgradeEffectResolver.ToLongCapacity(System.Math.Ceiling((double)(slot == 0 ? FirstWeapon.magazineSize : SecondWeapon.magazineSize) * (1d + runMagazineBonus.Value))); public float FireRate => runFireRate.Value; public float ReloadRate => runReloadRate.Value; public float RechargeRate => runRechargeRate.Value; public float RecoilRecoveryMultiplier => runRecoilRecovery.Value; public double RhythmRemaining => System.Math.Max(0, rhythmUntil.Value - Now); public double OverdriveRemaining => System.Math.Max(0, overdriveUntil.Value - Now); public double ReloadRemaining => System.Math.Max(0, (ActiveSlot == 0 ? weapon.Value.ReloadFirst : weapon.Value.ReloadSecond) - Now); public BuildLoadout Loadout => loadout.Value; public BuildStats Stats => stats.Value; public WeaponSnapshot WeaponState => weapon.Value; public AbilitySnapshot AbilityState => ability.Value; public float UltimateCharge => ability.Value.UltimateCharge; private bool currentAttackUltimate; public float LastConfirmedHitTime { get; private set; } = -10; public float LastConfirmedDamage { get; private set; } public string Feedback { get; private set; } = ""; public event System.Action DamageConfirmed; public event System.Action FeedbackReset; private readonly RecentDamageTracker recentDamage = new RecentDamageTracker(); public DamageResult LastDamageResult { get; private set; } public float RecentDamage(ulong targetId, uint lifetime) => recentDamage.GetTotal(targetId,lifetime,Time.unscaledTimeAsDouble); public IReadOnlyList AbilityDefinitions => abilityDefinitions; public void CopyActiveBuffs(List destination) { if(IsServer&&IsSpawned)SyncBuffsServer(); destination.Clear();if(activeBuffs==null)return; for(int i=0;i1)buffScratch.Add(new PlayerBuffSnapshot(haste.HasteSource,PlayerBuffEffect.MovementSpeed,haste.HasteMultiplier-1,haste.HasteUntil)); Expansion.GunWindow.CopyActiveBuffs(buffScratch,Now); PrototypeRunDirector.Instance?.Effects?.CopyActiveBuffs(OwnerClientId,Now,buffScratch); long finalRank=UpgradeRank(UpgradeId.FinalRound); if(Ammo==1&&finalRank>0)buffScratch.Add(new PlayerBuffSnapshot(EffectSourceKey.Core(UpgradeId.FinalRound,OwnerClientId),PlayerBuffEffect.FinalShell, UpgradeEffectResolver.ToFiniteFloat(UpgradeEffectResolver.Resolve(UpgradeId.FinalRound,finalRank).Primary-1),rank:finalRank)); vitals.CopyActiveBuffs(buffScratch,Now); bool found=false;PlayerBuffSnapshot ward=default; foreach(var player in PrototypePlayerVitals.ActivePlayers) { var actor=player.GetComponent(); if(actor==null||actor.Expansion==null||!actor.Expansion.TryGetWardBenefit(vitals,Now,out var candidate))continue; if(!found||candidate.Magnitude>ward.Magnitude||candidate.Magnitude==ward.Magnitude&&candidate.Source.CompareTo(ward.Source)<0){ward=candidate;found=true;} } if(found)buffScratch.Add(ward); } buffScratch.Sort((a,b)=>{int c=a.Source.CompareTo(b.Source);return c!=0?c:a.Effect.CompareTo(b.Effect);}); for(int i=0;i=activeBuffs.Count)activeBuffs.Add(buffScratch[i]);else if(!activeBuffs[i].Equals(buffScratch[i]))activeBuffs[i]=buffScratch[i];} while(activeBuffs.Count>buffScratch.Count)activeBuffs.RemoveAt(activeBuffs.Count-1); } public bool CanAct => IsSpawned && vitals.IsAlive && !PrototypeRunDirector.BlockCombat; public float MovementSpeed => Stats.MoveSpeed * (Now < ability.Value.HasteUntil ? ability.Value.HasteMultiplier : 1) * (passives != null ? passives.MoveMultiplier : 1); public float ServerNormalMaximumMoveSpeed => MovementSpeed * 1.6f; public float ServerMaximumMoveSpeed => System.Math.Max(ServerNormalMaximumMoveSpeed, Now < serverDashUntil&&(!serverSlipstream||serverSlipstreamTravel=serverDashMotionUntil)return false; float seconds=(float)(serverDashMotionUntil-Now),travel=serverSlipstream?Mathf.Max(0,serverSlipstreamBudget-serverSlipstreamTravel):float.MaxValue; if(!MovementRules.IsFinite(seconds)||!MovementRules.IsFinite(travel)||!MovementRules.IsFinite(serverDashSpeed) ||seconds<=0||travel<=0||serverDashSpeed<=0)return false; sequence=serverDashSequence;speed=serverDashSpeed;remainingSeconds=seconds;remainingTravel=travel;usesSlipstream=serverSlipstream;return true; } public bool NotifyAcceptedWarpDashTravelServer(uint expectedSequence,float travel) { if(!IsServer||!CanAct||expectedSequence==0||expectedSequence!=serverDashSequence ||!MovementRules.IsFinite(travel)||travel<0)return false; // The correction proof owns one-shot delivery. Charge only its captured dash // path, even if confirmation arrives after this dash's motion deadline. if(serverSlipstream)serverSlipstreamTravel=Mathf.Min(serverSlipstreamBudget,serverSlipstreamTravel+travel); if(movementAbilityEnds>0)CreditMovementAbilityTravel(travel); return true; } public bool ValidateSlipstreamStepServer(Vector3 previousAccepted,Vector3 proposed,double elapsed) { if(!serverSlipstream||Now>=serverDashUntil)return true; if(double.IsNaN(elapsed)||double.IsInfinity(elapsed)||elapsed<0)return false; Vector3 delta=proposed-previousAccepted;delta.y=0; return delta.magnitude<=System.Math.Max(0,serverSlipstreamBudget-serverSlipstreamTravel)+MovementSpeed*1.6*elapsed+.1; } public void NotifyAcceptedMovementServer(Vector3 previousAccepted,Vector3 nextAccepted) { if(!IsServer)return; Vector3 delta=nextAccepted-previousAccepted;delta.y=0; float travel=delta.magnitude; if(serverSlipstream&&Now0&&Now0)movementAbilityTravel=Mathf.Min(1,movementAbilityTravel+travel); } public Vector3 DashVelocity => Time.unscaledTime < localDashUntil && CanAct ? localDashVelocity : Vector3.zero; private double Now => IsSpawned ? NetworkManager.ServerTime.Time : Time.unscaledTimeAsDouble; private int ShotMask => ~LayerMask.GetMask("Players"); private bool RunStarted => PrototypeRunDirector.Instance != null ? PrototypeRunDirector.Instance.Phase != RunPhase.Menu : arena != null && arena.RunStarted; private ulong RunId => PrototypeRunDirector.Instance != null ? PrototypeRunDirector.Instance.RunId : 0; private ulong StageId => PrototypeRunDirector.Instance != null ? PrototypeRunDirector.Instance.StageId : 0; private bool CurrentStageRequest(ulong runId, ulong stageId) => runId == RunId && stageId == StageId; private void Awake() { activeBuffs=new NetworkList(null,NetworkVariableReadPermission.Owner,NetworkVariableWritePermission.Server); EffectSourceRegistry.Register(abilityDefinitions); movement = GetComponent(); vitals = GetComponent(); passives = GetComponent(); presentation = GetComponent(); Expansion=new AbilityExpansionRuntime(this); arena = Object.FindAnyObjectByType(); foreach (var definition in abilityDefinitions) { definitions.Add(definition.id, definition); catalog.Add(definition.id, definition.Tuning); } } public override void OnNetworkSpawn() { serverDashSequence=0; if(IsServer){if(++nextSpawnLifetime==0)++nextSpawnLifetime;spawnLifetime.Value=nextSpawnLifetime;} if (IsServer) { NetworkObject.DestroyWithScene = false; buildState = new PlayerBuildState(catalog, new AbilityUnlockState(catalog.Keys)); weaponState = new WeaponStateMachine(OwnerClientId, FirstWeapon.Tuning, SecondWeapon.Tuning); abilityState = new AbilityStateMachine(OwnerClientId, buildState); loadout.Value = buildState.Loadout; stats.Value = buildState.Stats; vitals.SetMaximumServer(buildState.Stats.MaximumHealth, true); SyncWeapon(); SyncAbilities(); } if (IsOwner) { LocalPlayer = this; RequestEquip(LocalBuildPreference.Selected); RequestWeapons(LocalWeaponPreference.First, LocalWeaponPreference.Second); } } public override void OnNetworkDespawn() { serverDashSequence=0; if(IsServer)PrototypeRunDirector.Instance?.Effects?.ClearOwnerTemporary(OwnerClientId);Expansion?.Clear();ClearExpansionVisuals();recentDamage.Clear(); FeedbackReset?.Invoke(); if (LocalPlayer == this) LocalPlayer = null; } public override void OnDestroy() { activeBuffs?.Dispose(); if (LocalPlayer == this) LocalPlayer = null; base.OnDestroy(); } private void Update() { if (!IsSpawned) return; TickExpansionVisuals(); if (IsServer) { TickHeldFireServer(); if (!serverHoldingFire || weaponState.NextAutomaticShotAt(nextHeldShot) > Now) weaponState.Tick(Now); abilityState.Ultimate.Tick(Now, vitals.IsAlive && PrototypeRunDirector.Instance?.Phase == RunPhase.Active); TickStarfallServer(); Expansion.Tick(Now); SyncExpansionState(); if(movementAbilityEnds>0&&Now>=movementAbilityEnds) { if(!CanAct)movementAbilityEnds=0; else if(movement.TryGetServerActionPosition(out var completedPosition)) { movementAbilityEnds=0; if(movementAbilityTravel>.5f) PrototypeRunDirector.Instance?.Effects?.MovementAbility(OwnerClientId,completedPosition,true,Now); } } SyncWeapon(); SyncAbilities(); if (ability.Value.HasteUntil != 0 && Now >= ability.Value.HasteUntil) { var snapshot = ability.Value; snapshot.HasteUntil = 0; snapshot.HasteMultiplier = 1; snapshot.HasteSource=default; ability.Value = snapshot; } } if(IsServer)SyncBuffsServer(); if (IsOwner) HandleInput(); } private void HandleInput() { var keyboard = Keyboard.current; var mouse = Mouse.current; bool controls = Application.isFocused && Cursor.lockState == CursorLockMode.Locked && !LocalPlayerSettings.IsInputBlocked && CanAct; float hipFov = LocalPlayerSettings.HipFieldOfView; movement.SetAim(controls && mouse != null && mouse.rightButton.isPressed ? hipFov * ActiveWeapon.aimFieldOfView / 80 : hipFov); if (controls && keyboard != null) { if (keyboard.digit1Key.wasPressedThisFrame) RequestSwitch(0); if (keyboard.digit2Key.wasPressedThisFrame) RequestSwitch(1); if (keyboard.rKey.wasPressedThisFrame) RequestReload(); if (keyboard.qKey.wasPressedThisFrame) RequestAbility(0, ActivationDirection(0)); if (keyboard.fKey.wasPressedThisFrame) RequestAbility(1, ActivationDirection(1)); if (keyboard.cKey.wasPressedThisFrame) RequestAbility(EquippedAbilitySlot.Third, ActivationDirection(EquippedAbilitySlot.Third)); if (keyboard.zKey.wasPressedThisFrame) RequestAbility(EquippedAbilitySlot.Ultimate, PlayerCamera.transform.forward); if (keyboard.vKey.wasPressedThisFrame) RequestAbility(EquippedAbilitySlot.Melee, PlayerCamera.transform.forward); } bool fireHeld = controls && mouse != null && mouse.leftButton.isPressed; bool firePressed = controls && mouse != null && mouse.leftButton.wasPressedThisFrame; #if UNITY_EDITOR || DEVELOPMENT_BUILD bool arrivalFire = movement.ReadOwnerArrivalFire(); fireHeld |= arrivalFire; firePressed |= arrivalFire && !arrivalFireWasHeld; arrivalFireWasHeld = arrivalFire; #endif bool holdFire = ActiveWeapon.automatic && fireHeld; if (holdFire != locallyHoldingFire || (holdFire && Time.unscaledTimeAsDouble >= nextHeldInput)) { locallyHoldingFire = holdFire; nextHeldInput = Time.unscaledTimeAsDouble + .1; RequestHeldFire(holdFire, PlayerCamera.transform.forward); } if ((ActiveWeapon.automatic ? fireHeld : firePressed) && Time.unscaledTimeAsDouble >= nextLocalShot && Ammo > 0 && ReloadRemaining <= 0 && Now >= weapon.Value.SwitchReady) { nextLocalShot = Time.unscaledTimeAsDouble + ActiveWeapon.shotInterval / runFireRate.Value; Vector3 direction = PlayerCamera.transform.forward; Vector3 end = PlayerCamera.transform.position + direction * ActiveWeapon.range; if (Physics.Raycast(PlayerCamera.transform.position, direction, out var hit, ActiveWeapon.range, ShotMask, QueryTriggerInteraction.Ignore)) end = hit.point; presentation.PlayShot(PlayerCamera.transform.position, end, ActiveWeapon); movement.AddRecoil(ActiveWeapon.recoilDegrees * SpreadMultiplier, UpgradeEffectResolver.ToFiniteFloat((double)ActiveWeapon.recoilRecovery * RecoilRecoveryMultiplier)); if (!ActiveWeapon.automatic) RequestFire(direction); } bool holding = controls && keyboard != null && keyboard.eKey.isPressed; if (holding != previouslyHoldingRevive || (holding && Time.unscaledTimeAsDouble >= nextReviveInput)) { previouslyHoldingRevive = holding; nextReviveInput = Time.unscaledTimeAsDouble + 0.2; RequestRevive(holding); } } public void RequestFire(Vector3 direction) { if (IsOwner && IsSpawned && weaponSequence < uint.MaxValue) FireRpc(++weaponSequence, direction, RunId, StageId); } public void RequestHeldFire(bool holding, Vector3 direction) { if (IsOwner && IsSpawned && heldSequence < uint.MaxValue) HeldFireRpc(++heldSequence, holding, direction, false, RunId, StageId); } public void ClearHeldInputForInspection() { locallyHoldingFire = false; if (IsOwner && IsSpawned && heldSequence < uint.MaxValue) HeldFireRpc(++heldSequence, false, Vector3.forward, true, RunId, StageId); previouslyHoldingRevive = false; RequestRevive(false); } private Vector3 ActivationDirection(int slot) => GetEquippedAbility(slot)?.UsesMovementInput == true ? movement.InputMoveDirection : PlayerCamera.transform.forward; public void RequestReload() { if (IsOwner && IsSpawned && weaponSequence < uint.MaxValue) ReloadRpc(++weaponSequence, RunId, StageId); } public void RequestSwitch(int slot) { if (IsOwner && IsSpawned && weaponSequence < uint.MaxValue) SwitchRpc(++weaponSequence, slot, RunId, StageId); } public void RequestAbility(int slot, Vector3 direction) { if (IsOwner && IsSpawned && abilitySequence < uint.MaxValue) AbilityRpc(++abilitySequence, slot, direction, RunId, StageId); } public void RequestEquip(BuildLoadout selected) { if (IsOwner && IsSpawned) EquipRpc(selected); } public void RequestWeapons(int firstId, int secondId) { if (IsOwner && IsSpawned) WeaponsRpc(firstId, secondId); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] private void WeaponsRpc(int firstId, int secondId, RpcParams rpc = default) { if(PrototypeSessionController.Instance?.RunLoadoutsLocked==true)return; if (rpc.Receive.SenderClientId != OwnerClientId || RunStarted || firstId == secondId || WeaponRoster.Get(firstId) == null || WeaponRoster.Get(secondId) == null) return; firstWeaponId.Value = firstId; secondWeaponId.Value = secondId; weaponState = new WeaponStateMachine(OwnerClientId, FirstWeapon.Tuning, SecondWeapon.Tuning); RecomputeRunModifiersServer(); SyncWeapon(); } public void RequestPracticeReset() { if (!IsOwner || !IsSpawned) return; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (IsServer && ProjectI.Debugging.PlaytestSceneController.TryRequestManualFixtureReset()) return; #endif PracticeResetRpc(); } public void RequestRevive(bool holding) { if (IsOwner && IsSpawned && reviveSequence < uint.MaxValue) vitals.RequestRevive(holding, ++reviveSequence); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] private void FireRpc(uint sequence, Vector3 direction, ulong runId, ulong stageId, RpcParams rpc = default) { if (!CurrentStageRequest(runId, stageId) || !CombatAuthorityRules.IsValidAim(direction) || !CanAct || !movement.TryGetServerActionPosition(out var position) || !weaponState.TryFire(rpc.Receive.SenderClientId, sequence, CanAct, Now)) return; ResolveAcceptedShotServer(direction, position); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] private void HeldFireRpc(uint sequence, bool holding, Vector3 direction, bool discardPending, ulong runId, ulong stageId, RpcParams rpc = default) { if (rpc.Receive.SenderClientId != OwnerClientId || sequence == 0 || sequence <= lastHeldSequence) return; lastHeldSequence = sequence; if (CurrentStageRequest(runId, stageId) && !holding && !discardPending) TickHeldFireServer(); if (!CurrentStageRequest(runId, stageId) || !holding || !CanAct || !ActiveWeapon.automatic || !CombatAuthorityRules.IsValidAim(direction) || !movement.TryGetServerActionPosition(out _)) { serverHoldingFire = false; return; } if (!serverHoldingFire || Now - lastHeldInput > .35) nextHeldShot = Now; serverHoldingFire = true; lastHeldInput = Now; heldAim = direction.normalized; } private void TickHeldFireServer() { if (!serverHoldingFire) return; if (!CanAct || !ActiveWeapon.automatic || Now - lastHeldInput > .35 || !movement.TryGetServerActionPosition(out var position)) { serverHoldingFire = false; return; } // Retain the next event when frame work is exhausted; never drop accepted shot identity. for (int i = 0; i < 512 && serverHoldingFire; i++) { double at = weaponState.NextAutomaticShotAt(nextHeldShot); if (at > Now) break; if (!weaponState.TryFireScheduled(CanAct, at)) break; nextHeldShot = at; scheduledEventTime = at; try { SyncWeapon(); ResolveAcceptedShotServer(heldAim, position); } finally { scheduledEventTime = null; } } } private void ResolveAcceptedShotServer(Vector3 direction, Vector3 position) { EnemyCombatNoiseEvents.EmitAcceptedWeaponShotServer(this, position, EventTimeServer); var tuning = weaponState.GetTuning(weaponState.ActiveSlot); Vector3 origin = position + Vector3.up * 1.6f; var definition = weaponState.ActiveSlot == 0 ? FirstWeapon : SecondWeapon; BeginAttackServer(); CurrentGunBonus=Expansion.GunWindow.Consume(EventTimeServer); currentAttackUltimate=CurrentGunBonus.Ultimate; PrototypeRunDirector.Instance?.Effects?.BeginWeaponTrigger(OwnerClientId); PrototypeRunDirector.Instance?.RecordWeaponShot(OwnerClientId, definition.weaponId, CurrentAttackId); var result = definition.ResolveShotServer(new WeaponShotContext(this, tuning, origin, direction, AttackDamageMultiplier, ShotMask)); PrototypeRunDirector.Instance?.CompleteAttack(OwnerClientId, CurrentAttackId); PrototypeRunDirector.Instance?.Effects?.EndWeaponTrigger(OwnerClientId,EventTimeServer); SyncWeapon(); ShotEffectsRpc(origin, result.Endpoint, weaponState.ActiveSlot); ProcessDamageResultServer(result.Result); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] private void ReloadRpc(uint sequence, ulong runId, ulong stageId, RpcParams rpc = default) { if (!CurrentStageRequest(runId, stageId) || rpc.Receive.SenderClientId != OwnerClientId) return; TickHeldFireServer(); if (weaponState.TryReload(rpc.Receive.SenderClientId, sequence, CanAct, Now)) SyncWeapon(); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] private void SwitchRpc(uint sequence, int slot, ulong runId, ulong stageId, RpcParams rpc = default) { if (!CurrentStageRequest(runId, stageId) || rpc.Receive.SenderClientId != OwnerClientId) return; TickHeldFireServer(); if (weaponState.TrySwitch(rpc.Receive.SenderClientId, sequence, slot, CanAct, Now)) { serverHoldingFire = false; PrototypeRunDirector.Instance?.NotifyWeaponSwitch(OwnerClientId); RecomputeRunModifiersServer(); SyncWeapon(); } } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] private void EquipRpc(BuildLoadout selected, RpcParams rpc = default) { if(PrototypeSessionController.Instance?.RunLoadoutsLocked==true)return; if (rpc.Receive.SenderClientId == OwnerClientId && CanAct && !RunStarted && selected.Equals(buildState.Loadout)) { FeedbackRpc("That build is already equipped."); return; } if (rpc.Receive.SenderClientId != OwnerClientId || !CanAct || !buildState.TryEquip(selected, RunStarted)) { FeedbackRpc("Build cannot be changed now, or a slot is invalid."); return; } ClearTemporaryEffectsServer(); loadout.Value = buildState.Loadout; stats.Value = buildState.Stats; vitals.SetMaximumServer(buildState.Stats.MaximumHealth); SyncAbilities(); FeedbackRpc("Build equipped."); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] private void AbilityRpc(uint sequence, int slot, Vector3 direction, ulong runId, ulong stageId, RpcParams rpc = default) { if (!CurrentStageRequest(runId, stageId) || !CombatAuthorityRules.IsValidAim(direction) || !CanAct || !movement.TryGetServerActionPosition(out var position))return; Vector3 origin = position + Vector3.up * 1.6f; var definition=GetEquippedAbility(slot); if(definition==null||!Expansion.Preflight(definition.Tuning,origin,direction,out preparedExpansionPoint) ||definition.Effect==AbilityEffect.Starfall&&!TryGroundTarget(origin,direction,out _,definition.Tuning.EffectParameters.PlacementMetres) || !abilityState.TryActivate(rpc.Receive.SenderClientId, sequence, slot, CanAct, Now, out var tuning)) return; BeginAttackServer(slot == EquippedAbilitySlot.Ultimate); bool normal=slot)(scale => { if(!capturedCast.Valid(this))return; float echoDamage = UpgradeEffectResolver.ToFiniteFloat((double)capturedDamage * scale); if (tuning.Effect == AbilityEffect.ArcLance) ApplyArcLanceServer(origin, direction, echoDamage, tuning.Radius, capturedAttackId, true); else if(tuning.Effect==AbilityEffect.Nova)capturedEffects.EchoArea(capturedOwner, capturedAttackId, origin, tuning.Radius, echoDamage); else Expansion.Echo(capturedCast,tuning,capturedPoint,scale); }) : null); effects?.GenuineCast(OwnerClientId,damaging,(tuning.Traits&AbilityTraits.Support)!=0,Now); } if (slot < EquippedAbilitySlot.NormalCount) passives?.NotifyActiveUsed(slot); SyncAbilities(); AbilityEffectsRpc(tuning.Id); } public void ApplyDashServer(Vector3 direction, AbilityTuning tuning) { if (!IsServer || !IsSpawned || !CanAct) return; Vector3 horizontal = new Vector3(direction.x, 0, direction.z); if (horizontal.sqrMagnitude < 0.01f) horizontal = transform.forward; serverDashUntil = Now + tuning.Duration + 0.35; serverDashMotionUntil = Now + tuning.Duration; // Exhaustion refuses extra correction allowance without wrapping an identity. serverDashSequence=nextServerDashSequence=value.HasteUntil||1+amount>=value.HasteMultiplier){value.HasteMultiplier=1+amount;value.HasteUntil=Now+duration;value.HasteSource=EffectSourceKey.Ability(sourceAbilityId,OwnerClientId);ability.Value=value;} } public void CreateDashWakeServer(Vector3 position,float duration) {if(IsServer&&CanAct&&movement.SpatiallySettled)Expansion.DashWake(position,duration);} public void EmitCorePulseServer(float amount,float radius) { if(!IsServer||!CanAct||!movement.TryGetServerActionPosition(out var position))return;Vector3 point=position+Vector3.up; ulong attack=BeginAttackServer();CombatSpatialQuery.DistinctDamageTargets(point,radius,areaHits,areaTargets); float damage=DamageRules.Scale(DamageKind.Ability,amount,Stats.AbilityDamageMultiplier,false,1); foreach(var target in areaTargets)ProcessDamageResultServer(target.ApplyDamageServer(new DamageContext(OwnerClientId,DamageKind.Ability,damage,false,target.AimPoint,attack,true))); ExpansionVisualServer(-2,point,radius,.3f,true); } public void GrantSharedShelterServer(float amount,float radius,double duration) =>GrantSharedShelterServer(amount,radius,duration,UpgradeEffectResolver.Resolve(UpgradeId.SharedShelter,1).TriggerInterval); public void GrantSharedShelterServer(float amount,float radius,double duration,double familyInterval) { if(!IsServer||!CanAct||!movement.TryGetServerActionPosition(out var position))return;var origin=position+Vector3.up; foreach(var ally in PrototypePlayerVitals.ActivePlayers) if(ally.IsSpawned&&ally.IsAlive&&(ally.AcceptedPosition-position).sqrMagnitude<=radius*radius &&!Physics.Linecast(origin,ally.AcceptedPosition+Vector3.up,~LayerMask.GetMask("Players","CombatTargets"),QueryTriggerInteraction.Ignore)) ally.GrantSharedShelterFamilyServer(amount,OwnerClientId,duration,familyInterval); } public bool EquipValidatedLobbyServer(BuildLoadout selected,int firstId,int secondId) { return EquipValidatedServer(selected, firstId, secondId, false); } public bool EquipDebugServer(BuildLoadout selected,int firstId,int secondId) { if (!ProjectI.Debugging.DebugArenaEntry.InArena || ProjectI.Debugging.DebugScenarioController.Instance == null || !ProjectI.Debugging.DebugScenarioController.Instance.IsServer) return false; return EquipValidatedServer(selected, firstId, secondId, true); } private bool EquipValidatedServer(BuildLoadout selected,int firstId,int secondId,bool debug) { var first=WeaponRoster.Get(firstId);var second=WeaponRoster.Get(secondId); if(!IsServer||!IsSpawned||(!debug&&RunStarted)||first==null||second==null||firstId==secondId||!buildState.TryEquip(selected,false))return false; ClearTemporaryEffectsServer();loadout.Value=buildState.Loadout;stats.Value=buildState.Stats; firstWeaponId.Value=firstId;secondWeaponId.Value=secondId;weaponState=new WeaponStateMachine(OwnerClientId,first.Tuning,second.Tuning); vitals.SetMaximumServer(stats.Value.MaximumHealth);RecomputeRunModifiersServer();SyncWeapon();SyncAbilities();return true; } private readonly Dictionary expansionVisuals=new Dictionary(); private readonly List expiredExpansionVisuals=new List(8); private static Material expansionMaterial; private MaterialPropertyBlock expansionCueColor; private ulong nextExpansionVisualSequence; private bool hasObservedVisualIdentity; private ulong observedVisualRun,observedVisualStage; private uint observedVisualLifetime; private static readonly Stack expansionVisualPool=new Stack(32); private static void ReturnExpansionVisual(GameObject value) {if(value==null)return;value.SetActive(false);if(expansionVisualPool.Count<32)expansionVisualPool.Push(value);else Destroy(value);} public void ExpansionVisualServer(int id,Vector3 point,float radius,float duration,bool impact,bool persistent=false,Vector3 direction=default,float coneHalfAngle=35) {if(impact)duration=.18f;if(IsServer&&IsSpawned)ExpansionVisualRpc(id,point,radius,duration,impact,persistent,direction,coneHalfAngle,RunId,StageId,SpawnLifetime,Now+duration,++nextExpansionVisualSequence);} [Rpc(SendTo.ClientsAndHost,InvokePermission=RpcInvokePermission.Server)] private void ExpansionVisualRpc(int id,Vector3 point,float radius,float duration,bool impact,bool persistent,Vector3 direction,float coneHalfAngle,ulong runId,ulong stageId,uint lifetime,double expiresAt,ulong sequence) { ulong previousSequence=expansionVisuals.TryGetValue(id,out var existing)?existing.Sequence:0; double remaining=AbilityVisualLifetime.Remaining(RunId,StageId,SpawnLifetime,runId,stageId,lifetime,expiresAt,Now,sequence,previousSequence);if(remaining<=0)return; if(expansionVisuals.TryGetValue(id,out var prior)){ReturnExpansionVisual(prior.Object);expansionVisuals.Remove(id);} if(expansionVisuals.Count>=16)return; Color color=id==108?Color.yellow:id==113?Color.green:id==303?new Color(.7f,.2f,1):Color.cyan; if(impact) { if(id==110)SphericalEffectPresentation.PlayCone(point,radius,color,direction,coneHalfAngle,expiresAt-duration,expiresAt); else SphericalEffectPresentation.Play(point,radius,color,expiresAt-duration,expiresAt); expansionVisuals[id]=(null,Time.unscaledTimeAsDouble+remaining,sequence);return; } if(persistent)SphericalEffectPresentation.Pulse(point,radius,color,.18); if(expansionMaterial==null){var shader=Shader.Find("Universal Render Pipeline/Unlit");if(shader==null)return;expansionMaterial=new Material(shader);} GameObject go=null;while(expansionVisualPool.Count>0&&go==null)go=expansionVisualPool.Pop(); if(go==null){go=new GameObject("Ability field cue");go.AddComponent();}go.SetActive(true); var line=go.GetComponent();line.sharedMaterial=expansionMaterial; line.useWorldSpace=false;line.loop=true;line.positionCount=16;line.widthMultiplier=impact?.09f:.035f; line.startColor=line.endColor=Color.white; if(expansionCueColor==null)expansionCueColor=new MaterialPropertyBlock();expansionCueColor.Clear();expansionCueColor.SetColor("_BaseColor",color);line.SetPropertyBlock(expansionCueColor); go.transform.position=point; for(int i=0;i<16;i++){float angle=i*Mathf.PI*2/16;line.SetPosition(i,new Vector3(Mathf.Cos(angle)*radius,.03f,Mathf.Sin(angle)*radius));} expansionVisuals[id]=(go,Time.unscaledTimeAsDouble+remaining,sequence); } public void ClearExpansionVisualServer(int id){if(IsServer&&IsSpawned)ClearExpansionVisualRpc(id,RunId,StageId,SpawnLifetime,++nextExpansionVisualSequence);} [Rpc(SendTo.ClientsAndHost,InvokePermission=RpcInvokePermission.Server)] private void ClearExpansionVisualRpc(int id,ulong runId,ulong stageId,uint lifetime,ulong sequence) {if(runId==RunId&&stageId==StageId&&lifetime==SpawnLifetime&&(!expansionVisuals.TryGetValue(id,out var value)||sequence>value.Sequence))ClearExpansionVisualLocal(id);} private void ClearExpansionVisualLocal(int id) {if(expansionVisuals.TryGetValue(id,out var value)){ReturnExpansionVisual(value.Object);expansionVisuals.Remove(id);}} [Rpc(SendTo.ClientsAndHost,InvokePermission=RpcInvokePermission.Server)] private void ClearExpansionVisualsRpc(ulong runId,ulong stageId,uint lifetime,ulong sequence) { if(runId!=RunId||stageId!=StageId||lifetime!=SpawnLifetime)return; expiredExpansionVisuals.Clear();foreach(var pair in expansionVisuals)if(pair.Value.Sequence<=sequence)expiredExpansionVisuals.Add(pair.Key); foreach(int id in expiredExpansionVisuals)ClearExpansionVisualLocal(id); } private void ClearExpansionVisuals(){foreach(var value in expansionVisuals.Values)ReturnExpansionVisual(value.Object);expansionVisuals.Clear();} private void TickExpansionVisuals() { if(hasObservedVisualIdentity&&(observedVisualRun!=RunId||observedVisualStage!=StageId||observedVisualLifetime!=SpawnLifetime))ClearExpansionVisuals(); hasObservedVisualIdentity=true;observedVisualRun=RunId;observedVisualStage=StageId;observedVisualLifetime=SpawnLifetime; expiredExpansionVisuals.Clear();foreach(var pair in expansionVisuals)if(Time.unscaledTimeAsDouble>=pair.Value.End)expiredExpansionVisuals.Add(pair.Key);foreach(int id in expiredExpansionVisuals)ClearExpansionVisualLocal(id); } public void ApplyStrikeServer(Vector3 origin, Vector3 direction, AbilityTuning tuning) { if (!IsServer || !IsSpawned || !CanAct) return; if (Physics.SphereCast(origin, 0.25f, direction, out var hit, tuning.Radius, ShotMask, QueryTriggerInteraction.Ignore)) { var receiver = hit.collider.GetComponentInParent(); if (receiver == null) return; float amount = DamageRules.Scale(DamageKind.Ability, tuning.Magnitude, AbilityDamageMultiplier, false, 1); ProcessDamageResultServer(receiver.ApplyDamageServer(new DamageContext(OwnerClientId, DamageKind.Ability, amount, false, hit.point, CurrentAttackId, isUltimate: currentAttackUltimate))); } } public void ApplyAreaDamageServer(Vector3 origin, float radius, float damage) { if (!IsServer || !IsSpawned || !CanAct) return; CombatSpatialQuery.DistinctDamageTargets(origin, radius, areaHits, areaTargets); foreach (var target in areaTargets) ProcessDamageResultServer(target.ApplyDamageServer(new DamageContext(OwnerClientId, DamageKind.Ability, damage, false, target.AimPoint, CurrentAttackId, isUltimate: currentAttackUltimate))); } public void ApplyNovaServer(AbilityActivationContext context) { if(!IsServer||!IsSpawned||!CanAct)return; CombatSpatialQuery.DistinctDamageTargets(context.Origin,context.Tuning.Radius,areaHits,areaTargets); float damage=DamageRules.Scale(DamageKind.Ability,context.Tuning.Magnitude,AbilityDamageMultiplier,false,1); foreach(var target in areaTargets) { ProcessDamageResultServer(target.ApplyDamageServer(new DamageContext(OwnerClientId,DamageKind.Ability,damage,false,target.AimPoint,CurrentAttackId))); if(target is PrototypeEnemy enemy&&enemy.IsAlive) enemy.ApplyStatusServer(new StatusApplication(OwnerClientId,EnemyStatusId.Stun,1,context.Tuning.EffectParameters.StunSeconds,CurrentAttackId,false,context.Tuning.EffectParameters.StunSeconds,context.Tuning.Id),Now); } ExpansionVisualServer(context.Tuning.Id,context.Origin,context.Tuning.Radius,.18f,true); } public void AddUltimateDamageServer(float applied, bool ultimate) { if (!IsServer || !IsSpawned || !vitals.IsAlive || PrototypeRunDirector.Instance?.Phase != RunPhase.Active || Now < abilityState.Ultimate.ActiveUntil) return; abilityState.Ultimate.AddDamage(applied, ultimate); SyncAbilities(); } public void AddUltimateKillServer(bool elite, bool boss, bool ultimate) { if (!IsServer || !IsSpawned || !vitals.IsAlive || PrototypeRunDirector.Instance?.Phase != RunPhase.Active || Now < abilityState.Ultimate.ActiveUntil) return; abilityState.Ultimate.AddKill(elite, boss, ultimate); SyncAbilities(); } public void ResetUltimateServer() { if (IsServer && IsSpawned && abilityState != null) { abilityState.Ultimate.Reset(); SyncAbilities(); } } private readonly RaycastHit[] lanceHits = new RaycastHit[64]; public void ApplyArcLanceServer(Vector3 origin, Vector3 direction, float amount, float range, ulong attackId = 0, bool secondary = false) { if (!IsServer || !IsSpawned || !CanAct || !CombatAuthorityRules.IsValidAim(direction)) return; int count = Physics.SphereCastNonAlloc(origin, .15f, direction.normalized, lanceHits, range, ShotMask, QueryTriggerInteraction.Ignore); var completeHits = count == lanceHits.Length ? Physics.SphereCastAll(origin, .15f, direction.normalized, range, ShotMask, QueryTriggerInteraction.Ignore) : lanceHits; if (completeHits != lanceHits) count = completeHits.Length; System.Array.Sort(completeHits, 0, count, Comparer.Create((a, b) => a.distance.CompareTo(b.distance))); areaTargets.Clear(); Vector3 end = origin + direction.normalized * range; for (int i = 0; i < count; i++) { var hit = completeHits[i]; var target = hit.collider.GetComponentInParent(); if (target == null) { end = hit.point; break; } if (!target.IsAlive || !areaTargets.Add(target)) continue; ProcessDamageResultServer(target.ApplyDamageServer(new DamageContext(OwnerClientId, DamageKind.Ability, amount, false, hit.point, attackId == 0 ? CurrentAttackId : attackId, secondary))); } ShotEffectsRpc(origin, end, ActiveSlot); } public void ApplyShieldPulseServer(AbilityTuning tuning, bool aegis = false) { if (!IsServer || !IsSpawned || !CanAct) return; Vector3 point = movement.ServerAcceptedPosition + Vector3.up; var source = EffectSourceKey.Ability(tuning.Id,OwnerClientId); foreach (var target in PrototypePlayerVitals.ActivePlayers) { if (!target.IsSpawned || !target.IsAlive || (target.AcceptedPosition - movement.ServerAcceptedPosition).sqrMagnitude > tuning.Radius * tuning.Radius) continue; if (target != vitals && Physics.Linecast(point, target.AcceptedPosition + Vector3.up, ~LayerMask.GetMask("Players", "CombatTargets"), QueryTriggerInteraction.Ignore)) continue; float grant = aegis ? target.MaximumShield - target.Shield + target.MaximumShield : tuning.Magnitude; target.GrantShieldServer(grant, true, source, tuning.Duration); } } private int starfallRemaining; private double nextStarfall; private Vector3 starfallPoint; private float starfallRadius, starfallDamage; private double starfallInterval; private static bool TryGroundTarget(Vector3 origin, Vector3 direction, out Vector3 point,float range=40) { point = default; if (!Physics.Raycast(origin, direction.normalized, out var hit, range, ~LayerMask.GetMask("Players", "CombatTargets"), QueryTriggerInteraction.Ignore) || hit.normal.y < .5f) return false; point = hit.point + Vector3.up * .15f; return true; } public void ApplyStarfallServer(AbilityActivationContext context) { if (!IsServer || !IsSpawned || !CanAct || !TryGroundTarget(context.Origin, context.Direction, out starfallPoint,context.Tuning.EffectParameters.PlacementMetres)) return; var parameters=context.Tuning.EffectParameters; starfallRemaining = parameters.StrikeCount; nextStarfall = Now + parameters.InitialStrikeDelay; starfallRadius = context.Tuning.Radius; starfallInterval=parameters.StrikeIntervalSeconds; starfallDamage = DamageRules.Scale(DamageKind.Ability, context.Tuning.Magnitude, AbilityDamageMultiplier, false, 1); StarfallVisualRpc(starfallPoint, starfallRadius, (float)(parameters.InitialStrikeDelay+(parameters.StrikeCount-1)*starfallInterval+.1), false); } private void TickStarfallServer() { if (starfallRemaining <= 0) return; if (!CanAct) { starfallRemaining = 0; ClearStarfallVisualsRpc(); return; } while (starfallRemaining > 0 && Now >= nextStarfall) { BeginAttackServer(true); ApplyAreaDamageServer(starfallPoint, starfallRadius, starfallDamage); PrototypeRunDirector.Instance?.CompleteAttack(OwnerClientId, CurrentAttackId); StarfallVisualRpc(starfallPoint, starfallRadius, .3f, true); starfallRemaining--; nextStarfall += starfallInterval; } } private readonly List starfallVisuals = new List(4); [Rpc(SendTo.ClientsAndHost, InvokePermission = RpcInvokePermission.Server)] private void StarfallVisualRpc(Vector3 point, float radius, float duration, bool impact) { starfallVisuals.RemoveAll(x => x == null); if (starfallVisuals.Count >= 4) return; var go = new GameObject(impact ? "Starfall impact" : "Starfall warning"); go.transform.position = point; go.AddComponent().Initialize(radius, duration, impact ? new Color(.5f, .85f, 1) : new Color(.4f, .6f, 1), impact); starfallVisuals.Add(go); } [Rpc(SendTo.ClientsAndHost, InvokePermission = RpcInvokePermission.Server)] private void ClearStarfallVisualsRpc() { foreach (var go in starfallVisuals) if (go != null) Destroy(go); starfallVisuals.Clear(); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] private void PracticeResetRpc(RpcParams rpc = default) { if (rpc.Receive.SenderClientId != Unity.Netcode.NetworkManager.ServerClientId || OwnerClientId != Unity.Netcode.NetworkManager.ServerClientId) return; foreach (var player in PrototypePlayerVitals.ActivePlayers) player.GetComponent().ResetPracticeServer(); foreach (var target in PrototypeHealthTarget.ActiveTargets) target.ResetServer(); UnityEngine.Object.FindAnyObjectByType()?.ResetServer(); } public void ResetPracticeServer() { if (!IsServer || !IsSpawned) return; ClearTemporaryEffectsServer(); weaponState.Reset(Now); observedReloadCompletions = weaponState.CompletedReloads; ResetFeedbackRpc(); abilityState.ResetCooldowns(); abilityState.Ultimate.Reset(); vitals.ResetServer(); movement.ServerTeleport(movement.SpawnPosition); SyncWeapon(); SyncAbilities(); } public void ClearTemporaryEffectsServer() { if (!IsServer || !IsSpawned) return; serverHoldingFire = false; serverDashUntil = 0; serverDashMotionUntil=0;serverDashSequence=0; movementAbilityEnds=0;movementAbilityTravel=0;Expansion.Clear();ClearExpansionVisualsRpc(RunId,StageId,SpawnLifetime,++nextExpansionVisualSequence); starfallRemaining = 0; ClearStarfallVisualsRpc(); movement.ResetJumpStateServer(); passives?.ClearServer(); PrototypeRunDirector.Instance?.Effects?.ClearOwnerTemporary(OwnerClientId); var snapshot = ability.Value; snapshot.HasteUntil = 0; snapshot.HasteMultiplier = 1; snapshot.HasteSource=default; ability.Value = snapshot; DashRpc(Vector3.zero, 0); } public AbilityDefinition GetEquippedAbility(int slot) { int id = Loadout.EquippedId(slot); definitions.TryGetValue(id, out var definition); return definition; } public double CooldownRemaining(int slot) => System.Math.Max(0, (slot == 0 ? ability.Value.ReadyFirst : slot == 1 ? ability.Value.ReadySecond : slot == 2 ? ability.Value.ReadyThird : slot == EquippedAbilitySlot.Melee ? ability.Value.ReadyMelee : 0) - Now); private void SyncWeapon() { if (weaponState.CompletedReloads > observedReloadCompletions) { uint count = weaponState.CompletedReloads - observedReloadCompletions; observedReloadCompletions = weaponState.CompletedReloads; for (uint i = 0; i < count; i++) { passives?.NotifyReloadCompleted(); PrototypeRunDirector.Instance?.NotifyReload(OwnerClientId, true); } } weapon.Value = new WeaponSnapshot { ActiveSlot = weaponState.ActiveSlot, AmmoFirst = weaponState.AmmoFirst, AmmoSecond = weaponState.AmmoSecond, ReloadFirst = weaponState.ReloadUntilFirst, ReloadSecond = weaponState.ReloadUntilSecond, SwitchReady = weaponState.ReadyAfterSwitch }; } private void SyncAbilities() { SyncExpansionState(); var snapshot = ability.Value; snapshot.ReadyFirst = abilityState.ReadyAt(0); snapshot.ReadySecond = abilityState.ReadyAt(1); snapshot.ReadyThird = abilityState.ReadyAt(2); snapshot.ReadyMelee = abilityState.ReadyAt(EquippedAbilitySlot.Melee); snapshot.ChargesFirst = abilityState.ChargesAvailable(0, Now); snapshot.ChargesSecond = abilityState.ChargesAvailable(1, Now); snapshot.CapacityFirst = abilityState.ChargeCapacity(0); snapshot.CapacitySecond = abilityState.ChargeCapacity(1); snapshot.NextFirst = abilityState.NextChargeAt(0); snapshot.NextSecond = abilityState.NextChargeAt(1); snapshot.DurationFirst = abilityState.RechargeDuration(0); snapshot.DurationSecond = abilityState.RechargeDuration(1); snapshot.ChargesThird = abilityState.ChargesAvailable(2, Now); snapshot.CapacityThird = abilityState.ChargeCapacity(2); snapshot.NextThird = abilityState.NextChargeAt(2); snapshot.DurationThird = abilityState.RechargeDuration(2); snapshot.UltimateCharge = Mathf.Floor(abilityState.Ultimate.Charge * 10) / 10; ability.Value = snapshot; } private void SyncExpansionState() { if(!IsServer||!IsSpawned||Expansion==null)return; confirmedFocusTriggers.Value=Expansion.GunWindow.FocusRemaining(Now);confirmedRailTriggers.Value=Expansion.GunWindow.RailRemaining(Now); confirmedFields.Value=Expansion.FieldCount;confirmedProjectiles.Value=Expansion.PendingCount;confirmedStatusDurationBonus.Value=StatusDurationBonus; } public void ReduceActiveCooldownsServer(double seconds) { if (!IsServer || !IsSpawned || !CanAct) return; abilityState.ReduceActiveCooldowns(EventTimeServer, seconds); SyncAbilities(); } public ulong BeginAttackServer(bool ultimate = false) { if (!IsServer || !IsSpawned) return 0; CurrentAttackId = ++nextAttackId; currentAttackUltimate = ultimate; return CurrentAttackId; } public long ChargesAvailable(int slot) => slot == 0 ? ability.Value.ChargesFirst : slot == 1 ? ability.Value.ChargesSecond : slot == 2 ? ability.Value.ChargesThird : slot == EquippedAbilitySlot.Ultimate ? UltimateCharge >= 100 ? 1 : 0 : slot == EquippedAbilitySlot.Melee && CooldownRemaining(slot) <= 0 ? 1 : 0; public long ChargeCapacity(int slot) => slot == 0 ? System.Math.Max(1, ability.Value.CapacityFirst) : slot == 1 ? System.Math.Max(1, ability.Value.CapacitySecond) : slot == 2 ? System.Math.Max(1, ability.Value.CapacityThird) : 1; public float ChargeProgress(int slot) { if (slot == EquippedAbilitySlot.Ultimate) return Mathf.Clamp01(UltimateCharge / 100); if (slot == EquippedAbilitySlot.Melee) return Mathf.Clamp01(1 - (float)(CooldownRemaining(slot) / System.Math.Max(.01, GetEquippedAbility(slot)?.cooldown ?? 1))); double next = slot == 0 ? ability.Value.NextFirst : slot == 1 ? ability.Value.NextSecond : ability.Value.NextThird; double duration = slot == 0 ? ability.Value.DurationFirst : slot == 1 ? ability.Value.DurationSecond : ability.Value.DurationThird; return ChargesAvailable(slot) >= ChargeCapacity(slot) || duration <= 0 ? 1 : Mathf.Clamp01(1 - (float)((next - Now) / duration)); } public double ChargeRemaining(int slot) => slot == EquippedAbilitySlot.Melee ? CooldownRemaining(slot) : slot == EquippedAbilitySlot.Ultimate ? 0 : ChargesAvailable(slot) >= ChargeCapacity(slot) ? 0 : System.Math.Max(0, (slot == 0 ? ability.Value.NextFirst : slot == 1 ? ability.Value.NextSecond : ability.Value.NextThird) - Now); public void ReduceActiveCooldownFractionServer(float fraction) { if (IsServer && IsSpawned) { abilityState.ReduceActiveCooldownFractions(EventTimeServer, fraction); SyncAbilities(); } } public void RecomputeRunModifiersServer() { if (!IsServer || !IsSpawned || buildState == null) return; var modifiers = PrototypeRunDirector.Instance != null ? PrototypeRunDirector.Instance.GetModifiers(OwnerClientId) : RunModifiers.None; var baseline = buildState.Stats; stats.Value = new BuildStats(UpgradeEffectResolver.ToFiniteFloat((double)baseline.MaximumHealth + modifiers.FlatHealth), UpgradeEffectResolver.ToFiniteFloat((double)baseline.MoveSpeed * (1d + modifiers.MovementBonus)), UpgradeEffectResolver.ToFiniteFloat((double)baseline.AttackDamageMultiplier + modifiers.AttackPower / 100d), UpgradeEffectResolver.ToFiniteFloat((double)baseline.AbilityDamageMultiplier + modifiers.AbilityPower / 100d)); vitals.ChangeRunMaximumServer(stats.Value.MaximumHealth); vitals.ChangeNormalShieldMaximumServer(UpgradeEffectResolver.ToFiniteFloat(50d+modifiers.FlatShield)); vitals.ChangeOvershieldCapacityServer(UpgradeEffectResolver.ToFiniteFloat(UpgradeEffectResolver.OvershieldCapacity(UpgradeRank(UpgradeId.ReloadWard), UpgradeRank(UpgradeId.AbilityAegis), vitals.MaximumShield))); runFireRate.Value = modifiers.FireRate; runReloadRate.Value = modifiers.ReloadRate; runRechargeRate.Value = modifiers.RechargeRate; runRecoilRecovery.Value = modifiers.RecoilRecoveryMultiplier; var timed = PrototypeRunDirector.Instance?.Effects?.TimedSnapshot(OwnerClientId, Now) ?? default; rhythmUntil.Value = timed.RhythmUntil; overdriveUntil.Value = timed.OverdriveUntil; runWeakPointBonus.Value = modifiers.WeakPointBonus; runSpreadMultiplier.Value = modifiers.SpreadMultiplier; runMagazineBonus.Value = modifiers.MagazineBonus; for (int slot = 0; slot < 2; slot++) { var source = slot == 0 ? FirstWeapon : SecondWeapon; weaponState.UpdateTuning(slot, new WeaponTuning(source.damage, UpgradeEffectResolver.ToLongCapacity(System.Math.Ceiling((double)source.magazineSize * (1d + modifiers.MagazineBonus))), source.shotInterval / (double)runFireRate.Value, source.reloadDuration / (double)modifiers.ReloadRate, source.range, source.automatic), Now); } for (int slot = 0; slot < EquippedAbilitySlot.NormalCount; slot++) { int id = Loadout.EquippedId(slot); long extra = modifiers.ExtraCharges(id); abilityState.ConfigureCharges(id, extra == long.MaxValue ? long.MaxValue : extra + 1, modifiers.RechargeRate, Now); } SyncWeapon(); SyncAbilities(); } public void CheckpointRecoveryServer() { if (!IsServer || !IsSpawned) return; ClearTemporaryEffectsServer(); RecomputeRunModifiersServer(); vitals.ResetServer(); weaponState.Reset(Now); abilityState.ResetCooldowns(); observedReloadCompletions = weaponState.CompletedReloads; SyncWeapon(); SyncAbilities(); } private void ApplySecondaryAreaServer(Vector3 origin, float radius, float amount, ulong attackId) { if (!IsServer || !IsSpawned || PrototypeRunDirector.Instance?.CombatAllowed == false) return; int count = Physics.OverlapSphereNonAlloc(origin, radius, areaHits, LayerMask.GetMask("CombatTargets"), QueryTriggerInteraction.Ignore); areaTargets.Clear(); for (int i = 0; i < count; i++) { var target = areaHits[i].GetComponentInParent(); if (target == null || !target.IsHostile || !target.IsAlive || !areaTargets.Add(target)) continue; if (Physics.Linecast(origin, target.AimPoint, out var hit, ShotMask, QueryTriggerInteraction.Ignore) && hit.collider.GetComponentInParent() == target) ProcessDamageResultServer(target.ApplyDamageServer(new DamageContext(OwnerClientId, DamageKind.Ability, amount, false, hit.point, attackId, true))); } } [Rpc(SendTo.ClientsAndHost, InvokePermission = RpcInvokePermission.Server)] private void ShotEffectsRpc(Vector3 origin, Vector3 end, int slot) { if (!IsOwner) presentation.PlayShot(origin, end, slot == 0 ? FirstWeapon : SecondWeapon); } [Rpc(SendTo.ClientsAndHost, InvokePermission = RpcInvokePermission.Server)] private void AbilityEffectsRpc(int id) { if (definitions.TryGetValue(id, out var definition)) presentation.PlayAbility(definition); } [Rpc(SendTo.Owner, InvokePermission = RpcInvokePermission.Server)] private void DashRpc(Vector3 velocity, float duration) { localDashVelocity = velocity; localDashUntil = Time.unscaledTime + duration; } [Rpc(SendTo.Owner, InvokePermission = RpcInvokePermission.Server)] private void HitConfirmedRpc(DamageResult result) { if (!result.HasDamage) return; LastConfirmedHitTime = Time.unscaledTime; LastConfirmedDamage = result.HitDamage; LastDamageResult = result; recentDamage.Add(result.TargetId,result.Lifetime,result.Applied,Time.unscaledTimeAsDouble); presentation.PlayConfirmedHit(result.Critical); DamageConfirmed?.Invoke(result); } [Rpc(SendTo.Owner, InvokePermission = RpcInvokePermission.Server)] private void ResetFeedbackRpc() { recentDamage.Clear(); LastConfirmedHitTime = -10; LastConfirmedDamage = 0; LastDamageResult = default; FeedbackReset?.Invoke(); } public void ProcessDamageResultServer(DamageResult result,bool emitHitFeedback=true) =>ProcessDamageFeedbackServer(result,false,emitHitFeedback); public void ProcessEchoDamageResultServer(DamageResult result) =>ProcessDamageFeedbackServer(result,true); private void ProcessDamageFeedbackServer(DamageResult result,bool suppressPassives,bool emitHitFeedback=true) { if (!IsServer || !IsSpawned || !result.HasDamage) return; if (DamageFeedbackPolicy.AllowsPassiveEvents(PrototypeRunDirector.Instance?.Effects?.IsEvaluatingEcho==true,suppressPassives)) { if (result.Critical) passives?.NotifyHeadshot(); if (result.Lethal && result.Hostile) passives?.NotifyEnemyKilled(); } if(emitHitFeedback)ConfirmHitFeedbackServer(result); } public void ConfirmHitFeedbackServer(DamageResult feedback) {if(IsServer&&IsSpawned&&feedback.HasDamage&&feedback.HitDamage>0&&!float.IsNaN(feedback.HitDamage)&&!float.IsInfinity(feedback.HitDamage))HitConfirmedRpc(feedback);} [Rpc(SendTo.Owner, InvokePermission = RpcInvokePermission.Server)] private void FeedbackRpc(string message) { Feedback = message; } } }