using System; using System.Collections.Generic; using System.Linq; namespace ProjectI.Runs { public enum UpgradeTier { Common, Uncommon, Rare, Legendary } public enum UpgradeId { ReinforcedPayload=1, AbilityConduit, VitalPlating, QuickLoader, ExpandedFeed, LightFrame, RecoveryCircuit, AcceleratedAction, SerratedPayload, SalvageSiphon, ReloadWard, PrecisionLens, ExecutionCircuit, FinalRound, Stabilizer, CombatRhythm, ShrapnelRelay, ReserveCell, ArcConductor, AbilityAegis, RuptureCore, PhaseRounds, OverdriveCore, EchoCast, CrimsonCascade, CloseRangeRounds, LongshotLens, SteadyTrigger, FieldBattery, ShieldRecovery, RescueLink, SprintShield, AirReserve, DashWake, ControlRelay, MarkedRounds, BreakerRounds, BackstepGuard, LinkedRecovery, EmergencyShell, RunCircuit, PulseEngine, HuntersRhythm, SharedShelter, ShieldCore } public sealed class UpgradeDefinition { public int Id { get; } public string Name { get; } public UpgradeTier Tier { get; } public CoreCategory Categories { get; } // Compatibility metadata for older payload readers; no authored rank cap. public long MaxRank => long.MaxValue; public string Description => UpgradeEffectResolver.DescribeValues((UpgradeId)Id,UpgradeEffectResolver.Resolve((UpgradeId)Id,1)); public string Restriction { get; } public string IconKey => ((UpgradeId)Id).ToString(); public UpgradeDefinition(UpgradeId id,string name,UpgradeTier tier,string restriction="", CoreCategory categories=CoreCategory.None) { Id=(int)id;Name=name;Tier=tier;Restriction=restriction;Categories=categories; } } public static class UpgradeCatalog { private static readonly UpgradeDefinition[] definitions = { D(UpgradeId.ReinforcedPayload,"Attack Damage Core",0,categories:CoreCategory.Weapons), D(UpgradeId.AbilityConduit,"Ability Damage Core",0,"Fixed healing/shield grants do not scale with ability damage.",categories:CoreCategory.Abilities), D(UpgradeId.VitalPlating,"Health Core",0,"Normal shield remains separate; cannot revive.",categories:CoreCategory.Survival), D(UpgradeId.QuickLoader,"Reload Core",0,categories:CoreCategory.Weapons|CoreCategory.Utility), D(UpgradeId.ExpandedFeed,"Magazine Core",0,"Preserves rounds; added capacity starts empty.",categories:CoreCategory.Weapons), D(UpgradeId.LightFrame,"Speed Core",0,"Walk/sprint only; authored dash remains separate.",categories:CoreCategory.Mobility), D(UpgradeId.RecoveryCircuit,"Cooldown Core",0,"Sequential Q/F/C recharge; no ultimate or melee refund.",categories:CoreCategory.Abilities|CoreCategory.Utility), D(UpgradeId.AcceleratedAction,"Fire Rate Core",0,"Semiautomatic fire requires individual trigger presses.",categories:CoreCategory.Weapons), D(UpgradeId.SerratedPayload,"Bleed Rounds",1,"One contribution per accepted trigger/target; contributions add; no offensive chains.",categories:CoreCategory.Weapons), D(UpgradeId.SalvageSiphon,"Kill Siphon",1,"Credited hostile kills only; healing cannot revive or exceed maximum HP.",categories:CoreCategory.Survival), D(UpgradeId.ReloadWard,"Reload Shield",1,"Completed real reload; same source replenishes/refreshes overflow.",categories:CoreCategory.Weapons|CoreCategory.Survival), D(UpgradeId.PrecisionLens,"Weakpoint Lens",1,"Requires an actual weak-point hit.",categories:CoreCategory.Weapons), D(UpgradeId.ExecutionCircuit,"Kill Recharge",1,"Carry work across missing charges; no stored full-capacity refund or extra casts.",categories:CoreCategory.Abilities|CoreCategory.Utility), D(UpgradeId.FinalRound,"Last Round",1,"Last shell once, including all pellets.",categories:CoreCategory.Weapons), D(UpgradeId.Stabilizer,"Recoil Core",1,"Authored zero-spread weapons stay zero.",categories:CoreCategory.Weapons|CoreCategory.Utility), D(UpgradeId.CombatRhythm,"Quick Swap",1,"One refreshed switching buff.",categories:CoreCategory.Weapons|CoreCategory.Utility), D(UpgradeId.ShrapnelRelay,"Splinter Rounds",2,"Once per accepted weapon trigger; applied-health budget; no offensive chains.",categories:CoreCategory.Weapons), D(UpgradeId.ReserveCell,"Spare Charge",2,"Selected equipped normal active; ultimate/melee excluded.",categories:CoreCategory.Abilities|CoreCategory.Utility), D(UpgradeId.ArcConductor,"Chain Rounds",2,"Other distinct visible enemies; walls block; no recursive arcs.",categories:CoreCategory.Weapons), D(UpgradeId.AbilityAegis,"Cast Shield",2,"Genuine normal Q/F/C cast; ultimate/echo excluded; one refreshed overflow source.",categories:CoreCategory.Abilities|CoreCategory.Survival), D(UpgradeId.RuptureCore,"Kill Blast",2,"Primary kills only; applied killing-hit damage excludes overkill; solid cover blocks.",categories:CoreCategory.Weapons|CoreCategory.Abilities), D(UpgradeId.PhaseRounds,"Piercing Rounds",2,"Walls stop; distinct enemies; no secondary offensive effects.",categories:CoreCategory.Weapons), D(UpgradeId.OverdriveCore,"Cast Overdrive",3,"Two different normal actives within 5s; ultimate excluded; refreshes one buff.",categories:CoreCategory.Weapons|CoreCategory.Abilities), D(UpgradeId.EchoCast,"Echo Cast",3,"Supported damaging normal active only; ultimate/melee excluded; no charge/cast/passive/proc events.",categories:CoreCategory.Abilities), D(UpgradeId.CrimsonCascade,"Bleed Burst",3,"Requires Serrated Payload; owned bleed only; cover blocks; preserve ultimate provenance.",categories:CoreCategory.Weapons), D(UpgradeId.CloseRangeRounds,"Close-Range Rounds",1,"Within 8 m; direct guns only.",CoreCategory.Weapons), D(UpgradeId.LongshotLens,"Longshot Lens",1,"Beyond 18 m; direct guns only.",CoreCategory.Weapons), D(UpgradeId.SteadyTrigger,"Steady Trigger",1,"Same target; max five steps; miss/switch/2 s gap resets.",CoreCategory.Weapons), D(UpgradeId.FieldBattery,"Field Battery",1,"Next damaging normal cast within 6 s; echo/ultimate excluded.",CoreCategory.Abilities|CoreCategory.Utility), D(UpgradeId.ShieldRecovery,"Shield Recovery",1,"Normal shield recharge only; no overshield.",CoreCategory.Survival), D(UpgradeId.RescueLink,"Rescue Link",1,"Valid nearby revive with line of sight.",CoreCategory.Survival|CoreCategory.Utility), D(UpgradeId.SprintShield,"Sprint Shield",1,"Two uninterrupted sprint seconds; once per 6 s.",CoreCategory.Mobility|CoreCategory.Survival), D(UpgradeId.AirReserve,"Air Reserve",2,"Accepted extra jump; once per 4 s; extra-jump passive required.",CoreCategory.Mobility|CoreCategory.Survival), D(UpgradeId.DashWake,"Dash Wake",2,"Movement ability required; one trail per owner.",CoreCategory.Mobility|CoreCategory.Abilities), D(UpgradeId.ControlRelay,"Control Relay",2,"Slow/mark ability required; hard control unchanged.",CoreCategory.Abilities|CoreCategory.Utility), D(UpgradeId.MarkedRounds,"Marked Rounds",2,"Own mark ability required; direct guns only.",CoreCategory.Weapons|CoreCategory.Abilities), D(UpgradeId.BreakerRounds,"Breaker Rounds",2,"Elite/boss targets only.",CoreCategory.Weapons), D(UpgradeId.BackstepGuard,"Backstep Guard",2,"Completed movement ability; once per 5 s.",CoreCategory.Mobility|CoreCategory.Survival), D(UpgradeId.LinkedRecovery,"Linked Recovery",2,"Valid revive; normal shield only, no overflow; once per 8 s; no proc credit.",CoreCategory.Survival|CoreCategory.Utility), D(UpgradeId.EmergencyShell,"Emergency Shell",3,"Surviving damage crossing below 25% HP; once per 30 s.",CoreCategory.Survival|CoreCategory.Utility), D(UpgradeId.RunCircuit,"Run Circuit",3,"Move at least 3 m/s for 3 s; normal recharge only.",CoreCategory.Mobility|CoreCategory.Utility), D(UpgradeId.PulseEngine,"Pulse Engine",3,"Every third genuine damaging normal cast; once per 5 s; secondary.",CoreCategory.Abilities|CoreCategory.Utility), D(UpgradeId.HuntersRhythm,"Hunter's Rhythm",3,"Two different direct gun kills within 4 s; one source.",CoreCategory.Weapons|CoreCategory.Mobility), D(UpgradeId.SharedShelter,"Shared Shelter",3,"Normal support cast; living allies/LOS; once per 8 s.",CoreCategory.Survival|CoreCategory.Utility), D(UpgradeId.ShieldCore,"Shield Core",0,"Normal shield capacity; HP unchanged; preserves missing shield.",CoreCategory.Survival) }; private static UpgradeDefinition D(UpgradeId id,string name,int tier,string restriction="", CoreCategory categories=CoreCategory.None) => new UpgradeDefinition(id,name,(UpgradeTier)tier,restriction,categories); public static IReadOnlyList All { get; } = Array.AsReadOnly(definitions); public static UpgradeDefinition Get(int id) => definitions.FirstOrDefault(x=>x.Id==id); } [Serializable] public sealed class UpgradeInventoryEntry { public int UpgradeId,AbilityId; public long Rank; public string Name; public UpgradeTier Tier; } [Serializable] public sealed class UpgradeCard { public int UpgradeId,AbilityId; public long CurrentRank,MaxRank; public string Name,Description,Restriction; public UpgradeTier Tier; } [Serializable] public sealed class UpgradeOffer { public long RewardId; public UpgradeTier Tier; public UpgradeCard[] Cards=Array.Empty(); public bool Exhausted; } public sealed class RunInventory { private readonly Dictionary ranks = new Dictionary(); private static long Key(int id,int ability) => ((long)id<<32)|(uint)ability; public long Rank(UpgradeId id,int ability=0) => ranks.TryGetValue(Key((int)id,ability),out long value)?value:0; public bool Add(int id,int ability=0) { var definition=UpgradeCatalog.Get(id); if(definition==null || (id!=(int)UpgradeId.ReserveCell && ability!=0) || (id==(int)UpgradeId.ReserveCell && ability<=0))return false; long key=Key(id,ability),rank=ranks.TryGetValue(key,out long old)?old:0; if(rank==long.MaxValue)return false; ranks[key]=checked(rank+1);return true; } public bool Remove(int id,int ability=0) { long key=Key(id,ability);if(!ranks.TryGetValue(key,out var rank)||rank<=0)return false; if(rank==1)ranks.Remove(key);else ranks[key]=rank-1;return true; } public UpgradeInventoryEntry[] Snapshot() => ranks.OrderBy(x=>x.Key).Select(x=> { int id=(int)(x.Key>>32);var d=UpgradeCatalog.Get(id); return new UpgradeInventoryEntry {UpgradeId=id,AbilityId=(int)x.Key,Rank=x.Value,Name=d.Name,Tier=d.Tier}; }).ToArray(); private ResolvedUpgradeValues Values(UpgradeId id)=>UpgradeEffectResolver.Resolve(id,Rank(id)); private float Value(UpgradeId id)=>UpgradeEffectResolver.ToFiniteFloat(Values(id).Primary); public RunModifiers Modifiers() { return new RunModifiers { FlatShield=Value(UpgradeId.ShieldCore), FlatHealth=UpgradeEffectResolver.ToFiniteFloat(Values(UpgradeId.VitalPlating).Primary+Values(UpgradeId.SalvageSiphon).Secondary), AttackPower=UpgradeEffectResolver.ToFiniteFloat(100*Values(UpgradeId.ReinforcedPayload).Primary), AbilityPower=UpgradeEffectResolver.ToFiniteFloat(100*Values(UpgradeId.AbilityConduit).Primary), MovementBonus=UpgradeEffectResolver.ToFiniteFloat(Values(UpgradeId.LightFrame).Primary-1), ReloadRate=Value(UpgradeId.QuickLoader),RechargeRate=Value(UpgradeId.RecoveryCircuit),FireRate=Value(UpgradeId.AcceleratedAction), MagazineBonus=Value(UpgradeId.ExpandedFeed),WeakPointBonus=Value(UpgradeId.PrecisionLens), SpreadMultiplier=UpgradeEffectResolver.FloatRemainder(Values(UpgradeId.Stabilizer).Primary), RecoilRecoveryRate=UpgradeEffectResolver.ToFiniteFloat(Values(UpgradeId.Stabilizer).Secondary), Charges=Snapshot().Where(x=>x.UpgradeId==(int)UpgradeId.ReserveCell).ToDictionary(x=>x.AbilityId,x=>x.Rank) }; } } public struct RunModifiers { public float FlatHealth,FlatShield,AttackPower,AbilityPower,MovementBonus,ReloadRate,RechargeRate,FireRate,MagazineBonus,WeakPointBonus,SpreadMultiplier,RecoilRecoveryRate; public float RecoilRecoveryMultiplier => RecoilRecoveryRate; public Dictionary Charges; public long ExtraCharges(int abilityId)=>Charges!=null&&Charges.TryGetValue(abilityId,out long n)?n:0; public static RunModifiers None => new RunModifiers {ReloadRate=1,RechargeRate=1,FireRate=1,SpreadMultiplier=1,RecoilRecoveryRate=1}; } public sealed class UpgradeEligibility { public int[] ActiveAbilities=Array.Empty();public int[] DamagingAbilities=Array.Empty();public string[] ActiveNames=Array.Empty(); public float MaxHealth,MaxNormalShield; public bool ExtraJump,MovementAbility,StatusAbility,MarkAbility,SupportAbility; public bool HasTeammates=true; public bool EchoEligibilitySpecified; public int[] EchoAbilities=Array.Empty(); } }