using System; using System.Collections.Generic; using ProjectI.Combat; using ProjectI.Enemies; using ProjectI.Builds; using UnityEngine; namespace ProjectI.Runs { public enum ShieldAmountBasis { Flat, HPFraction } public readonly struct ShotImpact { public readonly PrototypeEnemy Enemy; public readonly float Direct,Applied; public readonly Vector3 Point; public readonly bool Lethal; public ShotImpact(PrototypeEnemy enemy,float direct,float applied,Vector3 point,bool lethal) { Enemy=enemy;Direct=direct;Applied=applied;Point=point;Lethal=lethal; } } public readonly struct RunTimedEffects { public readonly double RhythmUntil,OverdriveUntil; public readonly float RhythmReloadRate,OverdriveReloadRate,OverdriveFireRate,OverdriveAbilityPower; public RunTimedEffects(double rhythmUntil,double overdriveUntil,float rhythmReload,float overdriveReload,float overdriveFire,float overdrivePower) {RhythmUntil=rhythmUntil;OverdriveUntil=overdriveUntil;RhythmReloadRate=rhythmReload;OverdriveReloadRate=overdriveReload;OverdriveFireRate=overdriveFire;OverdriveAbilityPower=overdrivePower;} } public sealed class RunEffectService { private sealed class Hit { public PrototypeEnemy Enemy; public double Direct,Applied; public Vector3 Point; public bool Lethal; } private sealed class Attack { public ulong Owner,Id; public DamageKind Kind; public bool IsUltimate; public readonly List Hits=new List(16); } private sealed class Bleed { public ulong Owner,Attack; public bool IsUltimate; public PrototypeEnemy Target; public uint Lifetime; public double Remaining,Rate,Last,End; } private sealed class Bolt { public ulong Owner,Attack; public bool IsUltimate; public PrototypeEnemy Ignore; public uint IgnoreLifetime; public Vector3 Position,Velocity; public double Damage,End,Last; public int VisualId; } private sealed class Echo { public double At; public ulong Owner; public int AbilityId; public bool IsUltimate; public float Scale; public Action Apply; } private sealed class Timed { public double RhythmUntil,OverdriveUntil,LastSwitch=-100,LastOverdrive=-100,LastWard=-100,LastAegis=-100,LastSiphon=-100,LastExecution=-100; public int LastAbility; public double LastCast=-100; } private readonly Dictionary<(ulong,ulong,bool),Attack> attacks=new Dictionary<(ulong,ulong,bool),Attack>(); private readonly List bleeds=new List(512); private readonly List bolts=new List(128); private readonly List echoes=new List(32); private readonly Dictionary<(ulong,(PrototypeEnemy,uint),bool),(PrototypeEnemy,double,ulong)> tickTotals=new Dictionary<(ulong,(PrototypeEnemy,uint),bool),(PrototypeEnemy,double,ulong)>(); private readonly Stack boltPool=new Stack(); private readonly Stack bleedPool=new Stack(); private readonly Dictionary timed=new Dictionary(); private sealed class ExpansionState { public double LastAir=-100,LastSprint=-100,LastBackstep=-100,LastRevive=-100,LastEmergency=-100,LastPulse=-100,LastShelter=-100; public double SprintSince=-1,MovingSince=-1,LastMoving=-100,BatteryUntil,HunterUntil,LastGunKill=-100,LastHit=-100; public ulong LastGunTarget,SteadyTarget; public uint LastGunLifetime,SteadyLifetime; public int SteadySteps,CastCount; public bool TriggerHit,MovingRecharge; } private readonly Dictionary expansion=new Dictionary(); public Func Actor; private ExpansionState Expansion(ulong owner) {if(!expansion.TryGetValue(owner,out var value)){value=new ExpansionState();expansion.Add(owner,value);}return value;} private readonly Func inventory; private readonly System.Random random; private readonly RaycastHit[] sweep=new RaycastHit[32]; private readonly Collider[] echoAreaHits=new Collider[64]; private readonly HashSet echoAreaTargets=new HashSet(); private int nextVisualId,activeVisuals; private bool echoIsUltimate,applyingEcho; public Action Heal,Refund; public Action Barrier; public Action ShieldGrant; public Func ShieldContext; public Action StatsChanged; public Action BoltSpawned; public Action BoltEnded; public Action Feedback; public Action ProcCue; public bool IsEvaluatingEcho=>applyingEcho; public int BleedCount=>bleeds.Count; public int ProjectileCount=>bolts.Count; public int PendingActionCount=>attacks.Count+bleeds.Count+bolts.Count+echoes.Count; public RunEffectService(Func inventories,int seed) { inventory=inventories;random=new System.Random(seed); } private long Rank(ulong owner,UpgradeId id)=>inventory(owner)?.Rank(id)??0; private ResolvedUpgradeValues Values(ulong owner,UpgradeId id)=>UpgradeEffectResolver.Resolve(id,Rank(owner,id)); // Rank zero disables the benefit, but condition history retains the authored boundaries. private ResolvedUpgradeValues ConditionValues(ulong owner,UpgradeId id)=>UpgradeEffectResolver.Resolve(id,Math.Max(1,Rank(owner,id))); private static float Physical(double value)=>UpgradeEffectResolver.ToFiniteFloat(value); private Timed State(ulong owner) {if(!timed.TryGetValue(owner,out var state)){state=new Timed();timed.Add(owner,state);}return state;} private void GrantShield(ulong owner,float amount,ShieldAmountBasis basis,double duration,EffectSourceKey source) {if(ShieldGrant!=null)ShieldGrant(owner,amount,basis,duration,source);else if(basis==ShieldAmountBasis.Flat)Barrier?.Invoke(owner,amount,duration,source);} public RunTimedEffects TimedSnapshot(ulong owner,double now) { var t=State(owner);var rhythm=Values(owner,UpgradeId.CombatRhythm);var overdrive=Values(owner,UpgradeId.OverdriveCore); return new RunTimedEffects(t.RhythmUntil,t.OverdriveUntil,now destination) { var t=State(owner);var e=Expansion(owner); AddBuff(owner,UpgradeId.CombatRhythm,PlayerBuffEffect.ReloadRate,Values(owner,UpgradeId.CombatRhythm).Primary,t.RhythmUntil,now,destination); var overdrive=Values(owner,UpgradeId.OverdriveCore); AddBuff(owner,UpgradeId.OverdriveCore,PlayerBuffEffect.ReloadRate,overdrive.Primary,t.OverdriveUntil,now,destination); AddBuff(owner,UpgradeId.OverdriveCore,PlayerBuffEffect.FireRate,overdrive.Primary,t.OverdriveUntil,now,destination); AddBuff(owner,UpgradeId.OverdriveCore,PlayerBuffEffect.AbilityDamage,overdrive.Secondary,t.OverdriveUntil,now,destination); AddBuff(owner,UpgradeId.HuntersRhythm,PlayerBuffEffect.MovementSpeed,Values(owner,UpgradeId.HuntersRhythm).Primary,e.HunterUntil,now,destination); var battery=Values(owner,UpgradeId.FieldBattery); if(battery.Primary>0&&e.BatteryUntil>now)destination.Add(new PlayerBuffSnapshot(EffectSourceKey.Core(UpgradeId.FieldBattery,owner),PlayerBuffEffect.AbilityDamage, Physical(battery.Primary),e.BatteryUntil,Rank(owner,UpgradeId.FieldBattery)){FiniteUses=true,RemainingTriggers=1}); var circuit=Values(owner,UpgradeId.RunCircuit); if(e.MovingRecharge)AddBuff(owner,UpgradeId.RunCircuit,PlayerBuffEffect.RechargeRate,circuit.Primary,e.LastMoving+circuit.TriggerInterval,now,destination); AddConditional(owner,UpgradeId.CloseRangeRounds,PlayerBuffEffect.CloseRange,destination); AddConditional(owner,UpgradeId.LongshotLens,PlayerBuffEffect.Longshot,destination); AddConditional(owner,UpgradeId.MarkedRounds,PlayerBuffEffect.MarkedTarget,destination); AddConditional(owner,UpgradeId.BreakerRounds,PlayerBuffEffect.EliteTarget,destination); var steady=Values(owner,UpgradeId.SteadyTrigger); if(e.SteadySteps>0&&e.SteadyTarget!=0&&now-e.LastHit<=steady.Duration&&Rank(owner,UpgradeId.SteadyTrigger)>0) destination.Add(new PlayerBuffSnapshot(EffectSourceKey.Core(UpgradeId.SteadyTrigger,owner),PlayerBuffEffect.SteadyTarget, Physical(steady.Primary*CoreConditionRules.LimitSteps(e.SteadySteps,steady)),e.LastHit+steady.Duration,Rank(owner,UpgradeId.SteadyTrigger),e.SteadySteps) {TargetId=e.SteadyTarget,TargetLifetime=e.SteadyLifetime}); } private void AddConditional(ulong owner,UpgradeId id,PlayerBuffEffect effect,List destination) { long rank=Rank(owner,id);if(rank>0)destination.Add(new PlayerBuffSnapshot(EffectSourceKey.Core(id,owner),effect,Physical(Values(owner,id).Primary),0,rank)); } private void AddBuff(ulong owner,UpgradeId id,PlayerBuffEffect effect,double amount,double until,double now,List destination) { long rank=Rank(owner,id);if(rank>0&&amount>0&&until>now)destination.Add(new PlayerBuffSnapshot(EffectSourceKey.Core(id,owner),effect,Physical(amount),until,rank)); } public RunModifiers Modifiers(ulong owner,double now) { var m=inventory(owner)?.Modifiers()??RunModifiers.None;var t=TimedSnapshot(owner,now); m.ReloadRate=Physical((double)m.ReloadRate+t.RhythmReloadRate+t.OverdriveReloadRate); m.FireRate=Physical((double)m.FireRate+t.OverdriveFireRate);m.AbilityPower=Physical((double)m.AbilityPower+t.OverdriveAbilityPower); var e=Expansion(owner);if(nowPhysical(Values(owner,UpgradeId.ShieldRecovery).Primary); public float ReviveRateMultiplier(ulong owner)=>Physical(Values(owner,UpgradeId.RescueLink).Primary); public float StatusDurationBonus(ulong owner)=>Physical(Values(owner,UpgradeId.ControlRelay).Primary); public void AcceptedExtraJump(ulong owner,double now) {var e=Expansion(owner);var v=Values(owner,UpgradeId.AirReserve);if(v.Primary>0&&now-e.LastAir>=v.TriggerInterval){e.LastAir=now;GrantShield(owner,Physical(v.Primary),ShieldAmountBasis.Flat,v.Duration,EffectSourceKey.Core(UpgradeId.AirReserve,owner));}} public void SampleMovement(ulong owner,float speed,bool sprinting,double now) { if(!Finite(now)||now<0||!Finite(speed)||speed<0)return;var e=Expansion(owner); var actor=Actor?.Invoke(owner);bool validSprint=sprinting&&speed>=(actor!=null?actor.MovementSpeed*1.15f:3) &&(actor==null||actor.CanAct&&actor.ReloadRemaining<=0); if(validSprint){if(e.SprintSince<0)e.SprintSince=now;var v=Values(owner,UpgradeId.SprintShield);if(now-e.SprintSince>=v.Secondary&&v.Primary>0&&now-e.LastSprint>=v.TriggerInterval){e.LastSprint=now;GrantShield(owner,Physical(v.Primary),ShieldAmountBasis.Flat,v.Duration,EffectSourceKey.Core(UpgradeId.SprintShield,owner));}} else e.SprintSince=-1; bool previous=e.MovingRecharge; var circuit=ConditionValues(owner,UpgradeId.RunCircuit); if(speed>=circuit.Secondary){if(e.MovingSince<0)e.MovingSince=now;e.LastMoving=now;if(CoreConditionRules.MovementEligible(speed,now-e.MovingSince,circuit))e.MovingRecharge=true;} else {e.MovingSince=-1;if(now-e.LastMoving>=circuit.TriggerInterval)e.MovingRecharge=false;} if(previous!=e.MovingRecharge)StatsChanged?.Invoke(owner); } public void MovementAbility(ulong owner,Vector3 position,bool completed,double now) { var e=Expansion(owner);if(completed){var v=Values(owner,UpgradeId.BackstepGuard);if(v.Primary>0&&now-e.LastBackstep>=v.TriggerInterval){e.LastBackstep=now;GrantShield(owner,Physical(v.Primary),ShieldAmountBasis.Flat,v.Duration,EffectSourceKey.Core(UpgradeId.BackstepGuard,owner));}} else if(Rank(owner,UpgradeId.DashWake)>0)Actor?.Invoke(owner)?.CreateDashWakeServer(position,Physical(Values(owner,UpgradeId.DashWake).Duration)); } public void Revived(ulong owner,ulong ally,double now) { var e=Expansion(owner);var v=Values(owner,UpgradeId.LinkedRecovery);if(v.Primary<=0||now-e.LastRevive0&&CoreConditionRules.EmergencyCrossing(beforeHp,afterHp,maxHp,v)&&now-e.LastEmergency>=v.TriggerInterval) {e.LastEmergency=now;GrantShield(owner,Physical(v.Primary),ShieldAmountBasis.Flat,v.Duration,EffectSourceKey.Core(UpgradeId.EmergencyShell,owner));} } public float ConsumeFieldBattery(ulong owner,bool damaging,double now) { var e=Expansion(owner);if(!damaging||now>=e.BatteryUntil)return 1;e.BatteryUntil=0;return Physical(1+Values(owner,UpgradeId.FieldBattery).Primary); } public void GenuineCast(ulong owner,bool damaging,bool support,double now) { var e=Expansion(owner);var battery=Values(owner,UpgradeId.FieldBattery);if(battery.Primary>0)e.BatteryUntil=now+battery.Duration; if(damaging) { var pulse=Values(owner,UpgradeId.PulseEngine);e.CastCount=pulse.Primary>0&&pulse.Count>0?(int)(((long)e.CastCount+1)%pulse.Count):0; if(e.CastCount==0&&pulse.Primary>0&&now-e.LastPulse>=pulse.TriggerInterval) {e.LastPulse=now;Actor?.Invoke(owner)?.EmitCorePulseServer(Physical(pulse.Primary),Physical(pulse.Radius));} } if(support){var shelter=Values(owner,UpgradeId.SharedShelter);if(shelter.Primary>0&&now-e.LastShelter>=shelter.TriggerInterval) {e.LastShelter=now;Actor?.Invoke(owner)?.GrantSharedShelterServer(Physical(shelter.Primary),Physical(shelter.Radius),shelter.Duration,shelter.TriggerInterval);}} } public void BeginWeaponTrigger(ulong owner){var e=Expansion(owner);e.TriggerHit=false;e.SprintSince=-1;} public void EndWeaponTrigger(ulong owner,double now) {var e=Expansion(owner);if(!e.TriggerHit){e.SteadyTarget=0;e.SteadyLifetime=0;e.SteadySteps=0;}} public void InvalidateCore(ulong owner,UpgradeId id) { var e=Expansion(owner); if(id==UpgradeId.ControlRelay)foreach(var enemy in PrototypeEnemy.ActiveEnemies)enemy.RemoveStatusDurationBonusServer(owner); if(id==UpgradeId.FieldBattery)e.BatteryUntil=0; if(id==UpgradeId.PulseEngine)e.CastCount=0; if(id==UpgradeId.HuntersRhythm)e.HunterUntil=0; if(id==UpgradeId.RunCircuit){e.MovingRecharge=false;e.MovingSince=-1;} if(id==UpgradeId.SteadyTrigger){e.SteadySteps=0;e.SteadyTarget=0;} if(id==UpgradeId.DashWake){Actor?.Invoke(owner)?.Expansion.RemoveField(-1);foreach(var enemy in PrototypeEnemy.ActiveEnemies)enemy.RemoveAbilityStatusSourceServer(owner,-1);} bool shieldSource=id==UpgradeId.AirReserve||id==UpgradeId.SprintShield||id==UpgradeId.BackstepGuard||id==UpgradeId.EmergencyShell||id==UpgradeId.ReloadWard||id==UpgradeId.AbilityAegis; if(shieldSource)Actor?.Invoke(owner)?.Vitals.RemoveOvershieldSourceServer(EffectSourceKey.Core(id,owner)); if(id==UpgradeId.SharedShelter)foreach(var player in PrototypePlayerVitals.ActivePlayers)player.RemoveOvershieldSourceServer(EffectSourceKey.Core(UpgradeId.SharedShelter,owner)); StatsChanged?.Invoke(owner); } public float WeaponConditionalMultiplier(ulong owner,PrototypeEnemy target,Vector3 origin,double now) { if(target==null)return 1;double bonus=0,range=(target.AimPoint-origin).magnitude; if(CoreConditionRules.RangeMatches(UpgradeId.CloseRangeRounds,range,ConditionValues(owner,UpgradeId.CloseRangeRounds)))bonus+=Values(owner,UpgradeId.CloseRangeRounds).Primary; else if(CoreConditionRules.RangeMatches(UpgradeId.LongshotLens,range,ConditionValues(owner,UpgradeId.LongshotLens)))bonus+=Values(owner,UpgradeId.LongshotLens).Primary; if(target.IsElite||target.IsBoss)bonus+=Values(owner,UpgradeId.BreakerRounds).Primary; if(target.IsMarked)bonus+=Values(owner,UpgradeId.MarkedRounds).Primary; var e=Expansion(owner);var steady=Values(owner,UpgradeId.SteadyTrigger); if(e.SteadyTarget==target.TargetId&&e.SteadyLifetime==target.Lifetime&&CoreConditionRules.WithinHitGap(now-e.LastHit,steady)) bonus+=steady.Primary*CoreConditionRules.LimitSteps(e.SteadySteps,steady); return Physical(1+bonus); } public float FinalRoundMultiplier(ulong owner,bool last)=>last?Physical(Values(owner,UpgradeId.FinalRound).Primary):1; public float PenetrationMultiplier(ulong owner)=>Physical(Values(owner,UpgradeId.PhaseRounds).Primary); public void Record(PrototypeEnemy enemy,DamageContext context,DamageResult result) { if(context.Secondary||context.AttackId==0||!result.HasDamage||enemy==null)return; var key=(context.SourceOwner,context.AttackId,context.IsUltimate); if(!attacks.TryGetValue(key,out var attack)) {attack=new Attack {Owner=context.SourceOwner,Id=context.AttackId,Kind=context.Kind,IsUltimate=context.IsUltimate};attacks.Add(key,attack);} Hit hit=attack.Hits.Find(x=>x.Enemy==enemy);if(hit==null){hit=new Hit {Enemy=enemy,Point=result.ImpactPoint};attack.Hits.Add(hit);} hit.Direct+=context.Amount;hit.Applied+=result.Applied;hit.Lethal|=result.Lethal; } public void Complete(ulong owner,ulong id,double now) {Complete(owner,id,now,false);Complete(owner,id,now,true);} private void Complete(ulong owner,ulong id,double now,bool isUltimate) { if(!attacks.TryGetValue((owner,id,isUltimate),out var attack))return;attacks.Remove((owner,id,isUltimate)); double budget=0;Hit primary=null; foreach(var h in attack.Hits) { budget+=h.Applied; if(primary==null||h.Applied>primary.Applied||(h.Applied==primary.Applied&&h.Enemy.TargetId0&&h.Enemy.IsAlive)AddBleed(owner,id,h.Enemy,h.Direct*bleed.Primary,now,attack.IsUltimate); if(h.Lethal) { var rupture=Values(owner,UpgradeId.RuptureCore); if(rupture.Primary>0)Blast(owner,id,h.Point,Physical(rupture.Radius),h.Applied*rupture.Primary,h.Enemy,attack.IsUltimate); var cascade=Values(owner,UpgradeId.CrimsonCascade); if(cascade.Primary>0) { double normal=RemainingOwnedBleed(owner,h.Enemy,false),ultimate=RemainingOwnedBleed(owner,h.Enemy,true),remaining=normal+ultimate; if(remaining>0) { double scale=Math.Min(h.Applied*cascade.Secondary,remaining*cascade.Primary)/remaining; Blast(owner,id,h.Point,Physical(cascade.Radius),normal*scale,h.Enemy,attack.IsUltimate); Blast(owner,id,h.Point,Physical(cascade.Radius),ultimate*scale,h.Enemy,true); } } } } if(primary==null||budget<=0||attack.Kind!=DamageKind.Weapon)return; var state=Expansion(owner);state.TriggerHit=true; if(Rank(owner,UpgradeId.SteadyTrigger)>0) {var steady=Values(owner,UpgradeId.SteadyTrigger);state.SteadySteps=state.SteadyTarget==primary.Enemy.TargetId&&state.SteadyLifetime==primary.Enemy.Lifetime&&CoreConditionRules.WithinHitGap(now-state.LastHit,steady)?CoreConditionRules.LimitSteps(state.SteadySteps+1,steady):1; state.SteadyTarget=primary.Enemy.TargetId;state.SteadyLifetime=primary.Enemy.Lifetime;state.LastHit=now;} foreach(var hit in attack.Hits)if(hit.Lethal) { var hunter=Values(owner,UpgradeId.HuntersRhythm); if(hunter.Primary>0) {if(CoreConditionRules.WithinActivationWindow(now-state.LastGunKill,hunter)&&(state.LastGunTarget!=hit.Enemy.TargetId||state.LastGunLifetime!=hit.Enemy.Lifetime)) {state.HunterUntil=now+hunter.Duration;StatsChanged?.Invoke(owner);} state.LastGunKill=now;state.LastGunTarget=hit.Enemy.TargetId;state.LastGunLifetime=hit.Enemy.Lifetime;} } var relay=Values(owner,UpgradeId.ShrapnelRelay);long copies=ProcCopies(relay.Primary); if(copies>0) { var payloads=new Dictionary(); for(int packet=0;packet0) { var targets=VisibleTargets(primary.Point,primary.Enemy,Physical(arc.Radius)); for(int i=0;i0?bleedPool.Pop():new Bleed();added.Owner=owner;added.Attack=attack;added.IsUltimate=ultimate;added.Target=target;added.Lifetime=target.Lifetime;added.Remaining=damage;added.Rate=(double)damage/duration;added.Last=now;added.End=now+duration;bleeds.Add(added); } public double RemainingBleed(PrototypeEnemy target){if(target==null)return 0;double total=0;foreach(var b in bleeds)if(b.Target==target&&b.Lifetime==target.Lifetime)total+=b.Remaining;return total;} private double RemainingBleed(PrototypeEnemy target,bool isUltimate){if(target==null)return 0;double total=0;foreach(var b in bleeds)if(b.Target==target&&b.Lifetime==target.Lifetime&&b.IsUltimate==isUltimate)total+=b.Remaining;return total;} private double RemainingOwnedBleed(ulong owner,PrototypeEnemy target,bool isUltimate){if(target==null)return 0;double total=0;foreach(var b in bleeds)if(b.Owner==owner&&b.Target==target&&b.Lifetime==target.Lifetime&&b.IsUltimate==isUltimate)total+=b.Remaining;return total;} public void Kill(ulong owner,double now) { var t=State(owner);var siphon=Values(owner,UpgradeId.SalvageSiphon);if(siphon.Primary>0&&now-t.LastSiphon>=siphon.TriggerInterval){t.LastSiphon=now;Heal?.Invoke(owner,Physical(siphon.Primary));} var execution=Values(owner,UpgradeId.ExecutionCircuit);if(execution.Primary>0&&now-t.LastExecution>=execution.TriggerInterval){t.LastExecution=now;Refund?.Invoke(owner,Physical(execution.Primary));} } public void Reload(ulong owner,bool nonEmpty,double now) {var t=State(owner);var ward=Values(owner,UpgradeId.ReloadWard);if(nonEmpty&&ward.Primary>0&&now-t.LastWard>=ward.TriggerInterval){t.LastWard=now;GrantShield(owner,Physical(ward.Primary),ShieldAmountBasis.Flat,ward.Duration,EffectSourceKey.Core(UpgradeId.ReloadWard,owner));}} public void Switch(ulong owner,double now) {var t=State(owner);var rhythm=Values(owner,UpgradeId.CombatRhythm);if(rhythm.Primary>0&&now-t.LastSwitch>=rhythm.TriggerInterval){t.LastSwitch=now;t.RhythmUntil=now+rhythm.Duration;StatsChanged?.Invoke(owner);}} public void Cast(ulong owner,int abilityId,bool damaging,double now,Action echo,bool isUltimate=false) { if(isUltimate||applyingEcho)return; var t=State(owner);var aegis=Values(owner,UpgradeId.AbilityAegis); if(Rank(owner,UpgradeId.AbilityAegis)>0&&now-t.LastAegis>=aegis.TriggerInterval) { t.LastAegis=now; if(ShieldContext!=null){var context=ShieldContext(owner);var resolved=UpgradeEffectResolver.Resolve(UpgradeId.AbilityAegis,Rank(owner,UpgradeId.AbilityAegis),context.MaxHealth,context.MaxNormalShield);GrantShield(owner,Physical(resolved.Primary),ShieldAmountBasis.Flat,resolved.Duration,EffectSourceKey.Core(UpgradeId.AbilityAegis,owner));} else {var resolved=UpgradeEffectResolver.Resolve(UpgradeId.AbilityAegis,Rank(owner,UpgradeId.AbilityAegis),1,0);GrantShield(owner,Physical(resolved.Primary),ShieldAmountBasis.HPFraction,resolved.Duration,EffectSourceKey.Core(UpgradeId.AbilityAegis,owner));} } var overdrive=Values(owner,UpgradeId.OverdriveCore); if(overdrive.Primary>0&&t.LastAbility!=0&&t.LastAbility!=abilityId&&CoreConditionRules.WithinActivationWindow(now-t.LastCast,overdrive)&&now-t.LastOverdrive>=overdrive.TriggerInterval) {t.LastOverdrive=now;t.OverdriveUntil=now+overdrive.Duration;StatsChanged?.Invoke(owner);} t.LastAbility=abilityId;t.LastCast=now; var value=Values(owner,UpgradeId.EchoCast);long copies=damaging&&echo!=null?ProcCopies(value.Primary):0; if(copies>0)echoes.Add(new Echo {Owner=owner,AbilityId=abilityId,At=now+value.Duration,Apply=echo,Scale=Physical(value.Secondary*copies),IsUltimate=echoIsUltimate}); } public void Tick(double now,float delta) { foreach(var pair in expansion) {var e=pair.Value;if(e.MovingRecharge&&now-e.LastMoving>=ConditionValues(pair.Key,UpgradeId.RunCircuit).TriggerInterval){e.MovingRecharge=false;StatsChanged?.Invoke(pair.Key);}if(e.HunterUntil>0&&now>=e.HunterUntil){e.HunterUntil=0;StatsChanged?.Invoke(pair.Key);}} var ticks=tickTotals;ticks.Clear(); for(int i=bleeds.Count-1;i>=0;i--) { var b=bleeds[i];if(b.Target==null||!b.Target.IsAlive||b.Lifetime!=b.Target.Lifetime){bleeds.RemoveAt(i);bleedPool.Push(b);continue;} if(now-b.Last<.25&&now=b.End?b.Remaining:Math.Min(b.Remaining,b.Rate*Math.Max(0,now-b.Last));b.Last=Math.Min(now,b.End);b.Remaining-=amount; var key=(b.Owner,(b.Target,b.Lifetime),b.IsUltimate);if(ticks.TryGetValue(key,out var old))ticks[key]=(b.Target,old.Item2+amount,b.Attack);else ticks.Add(key,(b.Target,amount,b.Attack)); if(now>=b.End||b.Remaining<=0){bleeds.RemoveAt(i);bleedPool.Push(b);} } foreach(var entry in ticks)Damage(entry.Key.Item1,entry.Value.Item3,entry.Value.Item1,entry.Value.Item2,entry.Value.Item1.AimPoint,entry.Key.Item3); for(int i=bolts.Count-1;i>=0;i--) { var b=bolts[i];if(now<=b.Last)continue; double elapsed=Math.Max(0,Math.Min(now,b.End)-b.Last);b.Last=Math.Min(now,b.End);Vector3 step=b.Velocity*(float)elapsed;float length=step.magnitude; int count=length>0?Physics.SphereCastNonAlloc(b.Position,.08f,step.normalized,sweep,length,~LayerMask.GetMask("Players"),QueryTriggerInteraction.Ignore):0; var hits=count==sweep.Length?Physics.SphereCastAll(b.Position,.08f,step.normalized,length,~LayerMask.GetMask("Players"),QueryTriggerInteraction.Ignore):sweep; if(hits!=sweep)count=hits.Length; RaycastHit? nearest=null;bool nearestWall=false; for(int h=0;h();if(b.Ignore!=null&&enemy==b.Ignore&&enemy.Lifetime==b.IgnoreLifetime)continue;bool wall=enemy==null; if(nearest==null||hits[h].distance();if(enemy!=null&&enemy.IsAlive)Damage(b.Owner,b.Attack,enemy,b.Damage,nearest.Value.point,b.IsUltimate);EndBolt(i);} else {b.Position+=step;if(now>=b.End)EndBolt(i);} } for(int i=echoes.Count-1;i>=0;i--)if(now>=echoes[i].At) { var e=echoes[i];echoes.RemoveAt(i);bool previous=echoIsUltimate,previousApplying=applyingEcho;echoIsUltimate=e.IsUltimate;applyingEcho=true; try{e.Apply?.Invoke(e.Scale);}finally{echoIsUltimate=previous;applyingEcho=previousApplying;} } foreach(var pair in timed){var t=pair.Value;bool changed=false;if(t.RhythmUntil>0&&now>=t.RhythmUntil){t.RhythmUntil=0;changed=true;}if(t.OverdriveUntil>0&&now>=t.OverdriveUntil){t.OverdriveUntil=0;changed=true;}if(changed)StatsChanged?.Invoke(pair.Key);} } private void EndBolt(int index) {var b=bolts[index];if(b.VisualId!=0){activeVisuals--;BoltEnded?.Invoke(b.VisualId);}bolts.RemoveAt(index);boltPool.Push(b);} private void Launch(ulong owner,ulong attack,Vector3 origin,PrototypeEnemy target,PrototypeEnemy ignore,float damage,double now,bool isUltimate) { Vector3 direction=(target.AimPoint-origin).normalized;if(direction.sqrMagnitude<.9f)return;origin-=direction*.18f; int visual=0;if(activeVisuals<128&&BoltSpawned!=null){if(nextVisualId==int.MaxValue)nextVisualId=0;visual=++nextVisualId;activeVisuals++;} var added=boltPool.Count>0?boltPool.Pop():new Bolt();added.Owner=owner;added.Attack=attack;added.IsUltimate=isUltimate||echoIsUltimate;added.Ignore=target==ignore?null:ignore;added.IgnoreLifetime=added.Ignore!=null?added.Ignore.Lifetime:0;added.Position=origin;added.Velocity=direction*24;added.Damage=damage;added.Last=now;double duration=Math.Max(2,(target.AimPoint-origin).magnitude/24d+.25);added.End=now+duration;added.VisualId=visual;bolts.Add(added);if(visual!=0)BoltSpawned.Invoke(visual,origin,direction*24,(float)duration); } private PrototypeEnemy PickTarget(Vector3 origin,PrototypeEnemy source,int index,float radius) {var list=VisibleTargets(origin,source,radius);return list.Count>0?list[index%list.Count]:(source!=null&&source.IsAlive&&Visible(origin,source)?source:null);} private static List VisibleTargets(Vector3 point,PrototypeEnemy exclude,float radius) {var list=new List();foreach(var e in PrototypeEnemy.ActiveEnemies)if(e!=exclude&&e.IsAlive&&(double)(e.AimPoint-point).sqrMagnitude<=(double)radius*radius&&Visible(point,e))list.Add(e);list.Sort((a,b)=>{int d=(a.AimPoint-point).sqrMagnitude.CompareTo((b.AimPoint-point).sqrMagnitude);return d!=0?d:a.TargetId.CompareTo(b.TargetId);});return list;} private static bool Visible(Vector3 point,PrototypeEnemy target) {Vector3 start=point+Vector3.up*.05f;return !Physics.Linecast(start,target.AimPoint,~LayerMask.GetMask("CombatTargets","Players"),QueryTriggerInteraction.Ignore);} public void EchoArea(ulong owner,ulong attack,Vector3 origin,float radius,float amount,bool isUltimate=false) { bool previousApplying=applyingEcho,previousUltimate=echoIsUltimate; applyingEcho=true;echoIsUltimate=isUltimate||previousUltimate; try { // Echo the original Nova shape and exact-origin cover query, including collider overlap. CombatSpatialQuery.DistinctDamageTargets(origin,radius,echoAreaHits,echoAreaTargets); foreach(var target in echoAreaTargets) { var result=target.ApplyDamageServer(new DamageContext(owner,DamageKind.Ability,amount,false,target.AimPoint,attack,true,echoIsUltimate)); if(result.HasDamage)Feedback?.Invoke(owner,result); } } finally {applyingEcho=previousApplying;echoIsUltimate=previousUltimate;} } private void Blast(ulong owner,ulong attack,Vector3 point,float radius,double damage,PrototypeEnemy exclude,bool isUltimate) { if(damage<=0)return; foreach(var target in VisibleTargets(point,exclude,radius))Damage(owner,attack,target,damage,target.AimPoint,isUltimate); EmitCue(new CoreProcCue(CoreProcKind.Blast,point,point,radius)); } private DamageResult Damage(ulong owner,ulong attack,PrototypeEnemy target,double amount,Vector3 point,bool isUltimate) {if(target==null||!target.IsAlive||amount<=0)return default;var result=target.ApplyDamageServer(new DamageContext(owner,DamageKind.Ability,Physical(amount),false,point,attack,true,isUltimate||echoIsUltimate));if(result.HasDamage)Feedback?.Invoke(owner,result);return result;} private void EmitCue(CoreProcCue cue) { // Cosmetic transport failures must never interrupt or change authoritative proc output. try{ProcCue?.Invoke(cue);}catch(Exception exception){Debug.LogWarning("Core proc cue unavailable: "+exception.GetType().Name);} } public void ClearOwnerTemporary(ulong owner) {expansion.Remove(owner);foreach(var enemy in PrototypeEnemy.ActiveEnemies)enemy.RemoveAbilityStatusSourceServer(owner,-1);foreach(var player in PrototypePlayerVitals.ActivePlayers)player.RemoveOvershieldSourceServer(EffectSourceKey.Core(UpgradeId.SharedShelter,owner));if(timed.TryGetValue(owner,out var t)){t.RhythmUntil=t.OverdriveUntil=0;t.LastAbility=0;t.LastCast=-100;StatsChanged?.Invoke(owner);}echoes.RemoveAll(x=>x.Owner==owner);} public void ClearCombat() {expansion.Clear();for(int i=bolts.Count-1;i>=0;i--)EndBolt(i);foreach(var b in bleeds)bleedPool.Push(b);bleeds.Clear();echoes.Clear();attacks.Clear();foreach(var pair in timed){pair.Value.RhythmUntil=pair.Value.OverdriveUntil=0;StatsChanged?.Invoke(pair.Key);}} public void Clear(){ClearCombat();timed.Clear();} private static bool Finite(double n)=>!double.IsNaN(n)&&!double.IsInfinity(n); } }