using System; using System.Globalization; namespace ProjectI.Runs { // Primary and Secondary are named in each case below; doubles prevent intermediate overflow. public struct ResolvedUpgradeValues { public double Primary, Secondary, Duration, Radius, TriggerInterval, CapacityBonus, ActivationWindow; public long Count; } public static class UpgradeEffectResolver { // This is a physical float representation ceiling, never an acquisition/rank limit. public const double NumericCeiling = float.MaxValue; private static readonly double MinimumExponent = Math.Log(double.Epsilon); private static readonly double MaximumExponent = Math.Log(double.MaxValue); public static double LogGrowth(long rank) => rank<=0?0:Math.Log(1d+rank,2); private static double ExtraLog(long rank) => rank<=0?0:Math.Log((double)rank,2); public static float ToFiniteFloat(double value) { if(double.IsNaN(value))throw new ArgumentOutOfRangeException(nameof(value)); return (float)Math.Max(-NumericCeiling,Math.Min(NumericCeiling,value)); } public static float FloatRemainder(double value) => value>0?Math.Max(float.Epsilon,ToFiniteFloat(value)):0; // Truncates nonnegative values; callers compute ceiling first for magazine capacity. public static long ToLongCapacity(double value) { if(double.IsNaN(value))throw new ArgumentOutOfRangeException(nameof(value)); return value<=0?0:value>=long.MaxValue?long.MaxValue:(long)value; } public static ResolvedUpgradeValues Resolve(UpgradeId id,long rank,float maxHealth=0,float maxNormalShield=0) { if(rank<0)throw new ArgumentOutOfRangeException(nameof(rank)); var v=new ResolvedUpgradeValues(); if(rank==0) { if(id==UpgradeId.QuickLoader||id==UpgradeId.LightFrame||id==UpgradeId.RecoveryCircuit||id==UpgradeId.AcceleratedAction||id==UpgradeId.FinalRound||id==UpgradeId.Stabilizer)v.Primary=1; if(id==UpgradeId.Stabilizer)v.Secondary=1; if(id==UpgradeId.ShieldRecovery||id==UpgradeId.RescueLink)v.Primary=1; return v; } double n=rank,q=(double)(rank-1),l=LogGrowth(rank),h=ExtraLog(rank); switch(id) { case UpgradeId.ReinforcedPayload: case UpgradeId.AbilityConduit: v.Primary=.08*n;break; // additive damage multiplier case UpgradeId.VitalPlating: v.Primary=15*n;break; // max HP case UpgradeId.QuickLoader: v.Primary=1+.10*l;break; // reload rate case UpgradeId.ExpandedFeed: v.Primary=.10*n;break; // base magazine fraction bonus case UpgradeId.LightFrame: v.Primary=1+.05*l;break; // walk/sprint multiplier case UpgradeId.RecoveryCircuit: v.Primary=1+.08*l;break; // normal active recharge rate case UpgradeId.AcceleratedAction: v.Primary=1+.04*l;break; // fire rate case UpgradeId.SerratedPayload: v.Primary=.15+.05*q;v.Duration=3;break; // direct shot fraction case UpgradeId.SalvageSiphon: v.Primary=3+q;v.Secondary=2*n;v.TriggerInterval=.4;break; // kill heal / max HP case UpgradeId.ReloadWard: v.Primary=5+3*q;v.CapacityBonus=3*q;v.Duration=3;v.TriggerInterval=3;break; case UpgradeId.PrecisionLens: v.Primary=.15*n;break; // additive weak point multiplier case UpgradeId.ExecutionCircuit: v.Primary=.03+.01*q;v.TriggerInterval=.5;break; // authored cooldown refund fraction case UpgradeId.FinalRound: double exponent=Math.Log(1.20)+q*Math.Log(1.05); v.Primary=exponent>=MaximumExponent?double.MaxValue:Math.Exp(exponent); break; case UpgradeId.Stabilizer: v.Primary=Math.Max(double.Epsilon,Math.Exp(Math.Max(MinimumExponent,n*Math.Log(.90))));v.Secondary=1+.05*l;break; // spread/recoil remainder / recovery rate case UpgradeId.CombatRhythm: v.Primary=.15+.05*q;v.Duration=2+.25*h;v.TriggerInterval=5;break; // reload rate addition case UpgradeId.ShrapnelRelay: v.Primary=.25+.10*q;v.Secondary=.20;v.Count=2;v.Radius=18+h;break; // intensity / applied hit fraction / packets / seek range case UpgradeId.ReserveCell: v.Primary=n;v.Count=rank;break; // additional selected active charges case UpgradeId.ArcConductor: v.Primary=.20+.05*q;v.Secondary=.20;v.Count=2;v.Radius=10+.75*h;break; case UpgradeId.AbilityAegis: v.CapacityBonus=.03*q*Math.Max(0,maxNormalShield);v.Primary=.10*Math.Max(0,maxHealth)+v.CapacityBonus;v.Duration=3;v.TriggerInterval=4;break; case UpgradeId.RuptureCore: v.Primary=.50+.15*q;v.Radius=3+.30*h;break; // applied killing hit fraction case UpgradeId.PhaseRounds: v.Primary=.50+.10*q;v.Count=rank;break; // original direct hit fraction / additional distinct targets case UpgradeId.OverdriveCore: v.Primary=.15*l;v.Secondary=.15*n;v.Duration=4+.50*h;v.TriggerInterval=12;v.ActivationWindow=5;break; // fire/reload addition / ability multiplier addition case UpgradeId.EchoCast: v.Primary=.20*n;v.Secondary=.35;v.Duration=.3;break; // intensity / original output fraction / delay case UpgradeId.CrimsonCascade: v.Primary=.50*n;v.Secondary=1+.50*q;v.Radius=3+.30*h;break; // remaining bleed release / applied killing budget fraction case UpgradeId.CloseRangeRounds: v.Primary=.10*l;v.Radius=8;break; case UpgradeId.LongshotLens: v.Primary=.12*l;v.Radius=18;break; case UpgradeId.SteadyTrigger: v.Primary=.02*l;v.Count=5;v.Duration=2;break; case UpgradeId.FieldBattery: v.Primary=.08*l;v.Duration=6;break; case UpgradeId.ShieldRecovery: v.Primary=1+.10*l;break; case UpgradeId.RescueLink: v.Primary=1+.15*l;break; case UpgradeId.SprintShield: v.Primary=4*l;v.Secondary=2;v.Duration=2;v.TriggerInterval=6;break; case UpgradeId.AirReserve: v.Primary=10*l;v.Duration=2;v.TriggerInterval=4;break; case UpgradeId.DashWake: v.Primary=.25;v.Radius=2;v.Duration=2+.4*h;break; case UpgradeId.ControlRelay: v.Primary=.15*l;break; case UpgradeId.MarkedRounds: case UpgradeId.BreakerRounds: v.Primary=.12*l;break; case UpgradeId.BackstepGuard: v.Primary=12*l;v.Duration=2;v.TriggerInterval=5;break; case UpgradeId.LinkedRecovery: v.Primary=8*l;v.TriggerInterval=8;break; case UpgradeId.EmergencyShell: v.Primary=30*l;v.Secondary=.25;v.Duration=4;v.TriggerInterval=30;break; case UpgradeId.RunCircuit: v.Primary=.12*l;v.Secondary=3;v.Duration=3;v.TriggerInterval=.5;break; case UpgradeId.PulseEngine: v.Primary=25*l;v.Count=3;v.Radius=4;v.TriggerInterval=5;break; case UpgradeId.HuntersRhythm: v.Primary=.12*l;v.Count=2;v.Duration=4;v.ActivationWindow=4;break; case UpgradeId.SharedShelter: v.Primary=12*l;v.Radius=5;v.Duration=3;v.TriggerInterval=8;break; case UpgradeId.ShieldCore: v.Primary=10*n;break; default: throw new ArgumentOutOfRangeException(nameof(id)); } return v; } public static double OvershieldCapacity(long wardRank,long aegisRank,float maxNormalShield) => Math.Max(0,maxNormalShield)+Resolve(UpgradeId.ReloadWard,wardRank).CapacityBonus+Resolve(UpgradeId.AbilityAegis,aegisRank,0,maxNormalShield).CapacityBonus; private static string N(double value)=>value.ToString(Math.Abs(value)>=1000000||(value!=0&&Math.Abs(value)<.001)?"0.###E+0":"0.###",CultureInfo.InvariantCulture); private static string P(double value)=>N(100*value)+"%"; public static string Describe(UpgradeId id,long rank,float maxHealth=0,float maxNormalShield=0) { string current=DescribeValues(id,Resolve(id,rank,maxHealth,maxNormalShield)); string next=rank==long.MaxValue?"Further stacks exceed the supported integer representation.":DescribeValues(id,Resolve(id,rank+1,maxHealth,maxNormalShield)); return "Current ("+rank.ToString(CultureInfo.InvariantCulture)+" stacks): "+current+"\nNext stack: "+next; } public static string DescribeValues(UpgradeId id,ResolvedUpgradeValues v) { string interval=v.TriggerInterval>0?" Trigger interval "+N(v.TriggerInterval)+"s.":""; string text; switch(id) { case UpgradeId.ReinforcedPayload:text="Guns gain +"+P(v.Primary)+" additive base damage.";break; case UpgradeId.AbilityConduit:text="Damaging abilities and offensive ultimates gain +"+P(v.Primary)+" additive base damage.";break; case UpgradeId.VitalPlating:text="+"+N(v.Primary)+" maximum HP; installation preserves missing HP and cannot revive.";break; case UpgradeId.QuickLoader:text=N(v.Primary)+"x reload rate; reload time = baseline / rate.";break; case UpgradeId.ExpandedFeed:text="+"+P(v.Primary)+" of base magazine capacity, rounded up; new capacity starts empty.";break; case UpgradeId.LightFrame:text=N(v.Primary)+"x walk/sprint speed; dash is separate.";break; case UpgradeId.RecoveryCircuit:text=N(v.Primary)+"x sequential normal-active recharge rate; ultimate and melee excluded.";break; case UpgradeId.AcceleratedAction:text=N(v.Primary)+"x fire rate; semiautomatic shots require individual presses.";break; case UpgradeId.SerratedPayload:text="Each weapon trigger/target adds "+P(v.Primary)+" of direct damage as bleed over "+N(v.Duration)+"s; no offensive chains.";break; case UpgradeId.SalvageSiphon:text="Credited kills heal "+N(v.Primary)+" HP; +"+N(v.Secondary)+" maximum HP. Cannot revive.";break; case UpgradeId.ReloadWard:text="A completed real reload grants "+N(v.Primary)+" shield; overflow lasts "+N(v.Duration)+"s; +"+N(v.CapacityBonus)+" overshield capacity. Same source refreshes.";break; case UpgradeId.PrecisionLens:text="+"+N(v.Primary)+" weak-point multiplier on actual weak-point hits.";break; case UpgradeId.ExecutionCircuit:text="Kills refund "+P(v.Primary)+" of authored base cooldown across sequential missing charges; no credit while full.";break; case UpgradeId.FinalRound:text=N(v.Primary)+"x last-shell direct damage, applied once to the whole shell.";break; case UpgradeId.Stabilizer:text=P(v.Primary)+" spread/recoil remaining; "+N(v.Secondary)+"x recoil recovery rate.";break; case UpgradeId.CombatRhythm:text="Switching adds "+N(v.Primary)+" reload rate for "+N(v.Duration)+"s; refreshes one buff.";break; case UpgradeId.ShrapnelRelay:text=ProcChanceText.Format(v.Primary)+" per weapon trigger, each with "+v.Count+" shards at "+P(v.Secondary)+" of applied-health damage; seek range "+N(v.Radius)+"m. Whole activations guaranteed, fractional remainder rolled; no chains.";break; case UpgradeId.ReserveCell:text="+"+v.Count+" charges for the selected Q/F/C active; new charges supplied once; sequential recharge and overlap rules remain.";break; case UpgradeId.ArcConductor:text=ProcChanceText.Format(v.Primary)+" per weapon trigger, each hitting up to "+v.Count+" distinct other visible enemies at "+P(v.Secondary)+" applied-health damage; radius "+N(v.Radius)+"m. Whole activations guaranteed, fractional remainder rolled; walls block; no chains.";break; case UpgradeId.AbilityAegis:text="Q/F/C casts grant "+N(v.Primary)+" shield; overflow lasts "+N(v.Duration)+"s; +"+N(v.CapacityBonus)+" overshield capacity. Grant uses 10% max HP plus capacity bonus from max normal shield; ultimate/echo excluded.";break; case UpgradeId.RuptureCore:text="Primary kills blast "+N(v.Radius)+"m for "+P(v.Primary)+" of applied killing-hit damage; cover blocks, overkill excluded, no secondary triggers.";break; case UpgradeId.PhaseRounds:text="Pierce "+v.Count+" additional distinct enemies for "+P(v.Primary)+" original hit damage each; walls stop, no compounding or secondary procs.";break; case UpgradeId.OverdriveCore:text="Two different normal actives within "+N(v.ActivationWindow)+"s add "+N(v.Primary)+" fire/reload rate and +"+P(v.Secondary)+" ability damage for "+N(v.Duration)+"s; refreshes one buff; ultimate excluded.";break; case UpgradeId.EchoCast:text=ProcChanceText.Format(v.Primary)+" per supported damaging normal cast, each at "+P(v.Secondary)+" original output after "+N(v.Duration)+"s; whole echoes guaranteed, fractional remainder rolled; no charge, cast, passive or proc events; ultimate/melee excluded.";break; case UpgradeId.CrimsonCascade:text="Primary kills release "+P(v.Primary)+" remaining owned bleed, bounded by "+P(v.Secondary)+" applied killing-hit damage, in "+N(v.Radius)+"m; requires Serrated Payload, cover blocks, no secondary triggers.";break; case UpgradeId.CloseRangeRounds:text="+"+P(v.Primary)+" direct gun damage within "+N(v.Radius)+"m.";break; case UpgradeId.LongshotLens:text="+"+P(v.Primary)+" direct gun damage beyond "+N(v.Radius)+"m.";break; case UpgradeId.SteadyTrigger:text="Successive direct hits on one target add +"+P(v.Primary)+" gun damage per step, up to "+v.Count+" steps; miss, target switch or "+N(v.Duration)+"s gap resets.";break; case UpgradeId.FieldBattery:text="Normal casts prime +"+P(v.Primary)+" damage on the next damaging normal cast within "+N(v.Duration)+"s; one stored bonus, echo/ultimate excluded.";break; case UpgradeId.ShieldRecovery:text="Normal shield recharge delay = baseline / "+N(v.Primary)+"; no overshield.";break; case UpgradeId.RescueLink:text=N(v.Primary)+"x valid revive interaction rate; range, line of sight and living rescuer required.";break; case UpgradeId.SprintShield:text="Sprint uninterrupted for "+N(v.Secondary)+"s to grant "+N(v.Primary)+" shield; overflow lasts "+N(v.Duration)+"s.";break; case UpgradeId.AirReserve:text="Accepted extra jumps grant "+N(v.Primary)+" shield; overflow lasts "+N(v.Duration)+"s; extra-jump passive required.";break; case UpgradeId.DashWake:text="Movement abilities leave one "+N(v.Radius)+"m trail slowing by "+P(v.Primary)+" for "+N(v.Duration)+"s.";break; case UpgradeId.ControlRelay:text="+"+P(v.Primary)+" authored slow/mark duration; hard control unchanged.";break; case UpgradeId.MarkedRounds:text="+"+P(v.Primary)+" direct gun damage against marked targets; own mark ability required.";break; case UpgradeId.BreakerRounds:text="+"+P(v.Primary)+" direct gun damage to elites and bosses.";break; case UpgradeId.BackstepGuard:text="Completed movement abilities grant "+N(v.Primary)+" shield; overflow lasts "+N(v.Duration)+"s.";break; case UpgradeId.LinkedRecovery:text="Valid revives grant "+N(v.Primary)+" normal shield to you and the revived ally; no revive-loop proc credit.";break; case UpgradeId.EmergencyShell:text="Surviving damage crossing below "+P(v.Secondary)+" max HP grants "+N(v.Primary)+" shield for "+N(v.Duration)+"s; lethal damage remains lethal.";break; case UpgradeId.RunCircuit:text="Move at least "+N(v.Secondary)+"m/s for "+N(v.Duration)+"s to gain +"+P(v.Primary)+" normal recharge rate; stops after "+N(v.TriggerInterval)+"s below threshold.";break; case UpgradeId.PulseEngine:text="Every "+v.Count+" genuine damaging normal casts emit a "+N(v.Radius)+"m burst for "+N(v.Primary)+" ability damage; secondary and nonrecursive.";break; case UpgradeId.HuntersRhythm:text="Two different direct gun kills within "+N(v.ActivationWindow)+"s grant +"+P(v.Primary)+" movement for "+N(v.Duration)+"s; refreshes one source.";break; case UpgradeId.SharedShelter:text="Normal support casts grant living allies within "+N(v.Radius)+"m/line of sight "+N(v.Primary)+" shield; overflow lasts "+N(v.Duration)+"s; echo excluded.";break; case UpgradeId.ShieldCore:text="+"+N(v.Primary)+" maximum normal shield; HP unchanged, preserves missing shield.";break; default:throw new ArgumentOutOfRangeException(nameof(id)); } return text+interval; } } }