using System.Collections.Generic; using ProjectI.Runs; using ProjectI.Enemies; using UnityEngine; namespace ProjectI.Combat { [CreateAssetMenu(menuName = "ProjectI/Weapons/Hitscan weapon")] public class HitscanWeaponDefinition : WeaponDefinition { private sealed class Impact { public IAuthoritativeDamageReceiver Target; public double Normal,Critical; public Vector3 NormalPoint,CriticalPoint,Direction; public float DisplayDamage; } private readonly struct SecondaryImpact { public readonly IAuthoritativeDamageReceiver Target;public readonly DamageContext Context; public SecondaryImpact(IAuthoritativeDamageReceiver target,DamageContext context){Target=target;Context=context;} } private readonly List phaseImpacts=new List(32); private readonly Dictionary impacts = new Dictionary(); private readonly RaycastHit[] penetrationHits = new RaycastHit[64]; public override WeaponFireResult ResolveShotServer(WeaponShotContext context) { if (context.Actor == null || !context.Actor.IsServer || !context.Actor.IsSpawned) return new WeaponFireResult(context.Origin,0); EnemyHitVolumeSync.FlushIfDirty(); impacts.Clear();phaseImpacts.Clear(); Vector3 end=context.Origin+context.Direction*context.Tuning.Range; int pellets=Mathf.Clamp(pelletCount,1,16); long phaseRank=context.Actor.UpgradeRank(UpgradeId.PhaseRounds); long finalRank=context.Actor.UpgradeRank(UpgradeId.FinalRound); double final=context.Actor.IsFinalRoundServer ? UpgradeEffectResolver.Resolve(UpgradeId.FinalRound,finalRank).Primary : 1; for(int pellet=0;pellet(); if(receiver==null||!receiver.IsAlive)continue; var box=hit.collider.GetComponent(); bool scan=box!=null&&box.isScanWeakPoint&&receiver is PrototypeEnemy exposed&&exposed.IsScanWeakPointActive(box,context.Actor.EventTimeServer); bool critical=scan||box!=null&&box.isHeadshot&&!box.isScanWeakPoint; float weak=(box!=null&&box.multiplierOverride>0?box.multiplierOverride:headshotMultiplier)+context.Actor.WeakPointBonus; float conditional=ProjectI.Runs.PrototypeRunDirector.Instance?.Effects?.WeaponConditionalMultiplier(context.Actor.OwnerClientId,receiver as PrototypeEnemy,context.Origin,context.Actor.EventTimeServer)??1; double amount=(double)context.Tuning.Damage*WeaponPattern.Falloff(hit.distance,falloffStart,falloffEnd,minimumDamageFraction)*final*context.DamageMultiplier*(critical?PrecisionDamageRules.Multiplier(weak,scan):1)*(conditional+context.Actor.CurrentGunBonus.DamageBonus); if(double.IsNaN(amount)||amount<=0||float.IsNaN(hit.point.x)||float.IsNaN(hit.point.y)||float.IsNaN(hit.point.z) ||float.IsInfinity(hit.point.x)||float.IsInfinity(hit.point.y)||float.IsInfinity(hit.point.z))continue; float admitted=UpgradeEffectResolver.ToFiniteFloat(amount); if(receiver is PrototypeEnemy admittedEnemy)admitted=admittedEnemy.ResolveIncomingHitDamage(admitted); if(float.IsNaN(admitted)||float.IsInfinity(admitted)||admitted<=0)continue; if(scan) { var enemy=(PrototypeEnemy)receiver;var snapshot=enemy.ScanWeakPoints; if(!enemy.TryConsumeScanWeakPointServer(box.scanSlot,snapshot.Generation,snapshot.Lifetime,context.Actor.EventTimeServer))continue; } if(!impacts.TryGetValue(receiver,out var impact)){impact=new Impact{Target=receiver,Direction=direction};impacts.Add(receiver,impact);} if(critical){impact.Critical+=amount;impact.CriticalPoint=hit.point;}else{impact.Normal+=amount;impact.NormalPoint=hit.point;} if(phaseRank>0) Penetrate(context,direction,receiver,UpgradeEffectResolver.ToFiniteFloat(amount*UpgradeEffectResolver.Resolve(UpgradeId.PhaseRounds,phaseRank).Primary),phaseRank); } // Capture the accepted direct packets before Phase, body or precision damage mutates health. foreach(var impact in impacts.Values) {float normal=UpgradeEffectResolver.ToFiniteFloat(impact.Normal),precision=UpgradeEffectResolver.ToFiniteFloat(impact.Critical); if(impact.Target is PrototypeEnemy enemy){normal=enemy.ResolveIncomingHitDamage(normal);precision=enemy.ResolveIncomingHitDamage(precision);} impact.DisplayDamage=DamageRules.SumHitDamage(normal,precision);} // Preserve Phase-before-direct accounting, while gathering all packets before the first mutation. foreach(var impact in phaseImpacts)context.Actor.ProcessDamageResultServer(impact.Target.ApplyDamageServer(impact.Context)); Impact primary=null;double triggerDamage=0; foreach(var impact in impacts.Values){triggerDamage+=impact.Normal+impact.Critical;if(primary==null||impact.Normal+impact.Critical>primary.Normal+primary.Critical||impact.Normal+impact.Critical==primary.Normal+primary.Critical&&impact.Target.TargetId0&&primary!=null?CollectPenetrationHits(context.Origin,primary.Direction,context.Tuning.Range,context.CollisionMask,primary.Target,context.Actor.CurrentGunBonus.AdditionalPierceTargets):System.Array.Empty(); foreach(var impact in impacts.Values) { DamageResult normal=default,critical=default; if(impact.Normal>0){normal=impact.Target.ApplyDamageServer(new DamageContext(context.Actor.OwnerClientId,DamageKind.Weapon,UpgradeEffectResolver.ToFiniteFloat(impact.Normal),false,impact.NormalPoint,context.AttackId,isUltimate:context.Actor.CurrentGunBonus.Ultimate));context.Actor.ProcessDamageResultServer(normal,false);} if(impact.Critical>0){critical=impact.Target.ApplyDamageServer(new DamageContext(context.Actor.OwnerClientId,DamageKind.Weapon,UpgradeEffectResolver.ToFiniteFloat(impact.Critical),true,impact.CriticalPoint,context.AttackId,isUltimate:context.Actor.CurrentGunBonus.Ultimate));context.Actor.ProcessDamageResultServer(critical,false);} context.Actor.ConfirmHitFeedbackServer(DamageRules.AggregateFeedback(normal,critical,impact.DisplayDamage,impact.Critical>0)); } foreach(var hit in railTargets) {var receiver=hit.collider.GetComponentInParent();if(receiver!=null)context.Actor.ProcessDamageResultServer(receiver.ApplyDamageServer(new DamageContext(context.Actor.OwnerClientId,DamageKind.Ability,UpgradeEffectResolver.ToFiniteFloat(triggerDamage*context.Actor.CurrentGunBonus.PierceFraction),false,hit.point,context.AttackId,true,context.Actor.CurrentGunBonus.Ultimate)));} return new WeaponFireResult(end,0); } private void Penetrate(WeaponShotContext context,Vector3 direction,IAuthoritativeDamageReceiver first,float amount,long rank) { foreach(var hit in CollectPenetrationHits(context.Origin,direction,context.Tuning.Range,context.CollisionMask,first,rank)) { var receiver=hit.collider.GetComponentInParent(); phaseImpacts.Add(new SecondaryImpact(receiver,new DamageContext(context.Actor.OwnerClientId,DamageKind.Ability,amount,false,hit.point,context.AttackId,true,context.Actor.CurrentGunBonus.Ultimate))); } } public RaycastHit[] CollectPenetrationHits(Vector3 origin,Vector3 direction,float range,int collisionMask,IAuthoritativeDamageReceiver first,long additionalTargets) { if(additionalTargets<=0 || first==null)return System.Array.Empty(); int count=Physics.RaycastNonAlloc(origin,direction,penetrationHits,range,collisionMask,QueryTriggerInteraction.Ignore); // A full buffer may contain any subset of the ray. Complete it before sorting, so a // discarded early wall or target never changes authoritative penetration. var hits=count==penetrationHits.Length?Physics.RaycastAll(origin,direction,range,collisionMask,QueryTriggerInteraction.Ignore):penetrationHits; if(hits!=penetrationHits)count=hits.Length; System.Array.Sort(hits,0,count,Comparer.Create((a,b)=> { int distance=a.distance.CompareTo(b.distance); if(distance!=0)return distance; bool aWall=a.collider.GetComponentInParent()==null; bool bWall=b.collider.GetComponentInParent()==null; return aWall==bWall?0:aWall?-1:1; })); var seen=new HashSet{first}; var selected=new List(); bool passed=false; for(int i=0;i(); if(receiver==null)break; if(receiver==first){passed=true;continue;} if(!passed||!receiver.IsAlive||!seen.Add(receiver))continue; selected.Add(hit); if((long)selected.Count>=additionalTargets)break; } return selected.ToArray(); } } }