using System; using System.Collections.Generic; using System.Linq; namespace ProjectI.Runs { public sealed class RunRewardService { private sealed class Queue { public readonly int[] Counts=new int[4], Deferred=new int[4]; public UpgradeOffer Offer; public CoreCategoryOffer Category; public CoreAwardReceipt Receipt; public ulong DeferredStage; public readonly Dictionary Pools=new Dictionary(); public readonly Dictionary Receipts=new Dictionary(); public UpgradeEligibility Eligibility; public long Stream; } private readonly Dictionary queues=new Dictionary(); private readonly HashSet deaths=new HashSet(); private readonly Random random; private int drought; private long nextOffer; public RunRewardService(int seed) { random=new Random(seed); } public void BeginStage() { deaths.Clear(); } public bool RollDeath(ulong target,bool elite,bool boss,out UpgradeTier tier) { tier=UpgradeTier.Common; if(!deaths.Add(target))return false; bool drop=boss || drought>=15 || random.NextDouble()<(elite?.25:.08); if(!drop){drought++;return false;} drought=0; double roll=random.NextDouble(); tier=boss?(roll<.50?UpgradeTier.Common:roll<.80?UpgradeTier.Uncommon:roll<.98?UpgradeTier.Rare:UpgradeTier.Legendary) :(roll<.65?UpgradeTier.Common:roll<.90?UpgradeTier.Uncommon:roll<.99?UpgradeTier.Rare:UpgradeTier.Legendary); return true; } public void Bank(IEnumerable owners,UpgradeTier tier) { foreach(var owner in owners) { var q=Get(owner); if(q.Counts[(int)tier]queues.TryGetValue(owner,out var q)?q.Offer:null; public CoreCategoryOffer LockedCategory(ulong owner)=>queues.TryGetValue(owner,out var q)?q.Category:null; public CoreAwardReceipt LockedReceipt(ulong owner)=>queues.TryGetValue(owner,out var q)?q.Receipt:null; public int DeferredPending(ulong owner) {if(!queues.TryGetValue(owner,out var q))return 0;return (int)Math.Min(int.MaxValue,q.Deferred.Sum(x=>(long)x));} private static void AdvanceDeferred(Queue q,ulong stage) { if(q.DeferredStage==stage)return; for(int i=0;i<4;i++){q.Counts[i]=(int)Math.Min(int.MaxValue,(long)q.Counts[i]+q.Deferred[i]);q.Deferred[i]=0;} foreach(var id in q.Receipts.Where(x=>x.Value.StageId!=stage).Select(x=>x.Key).ToArray())q.Receipts.Remove(id); q.DeferredStage=stage; } public int BlockingPending(ulong owner,ulong stage) { if(!queues.TryGetValue(owner,out var q))return 0; AdvanceDeferred(q,stage);long total=q.Offer!=null||q.Category!=null?1:0; if(q.Receipt!=null)total++; foreach(var n in q.Counts)total+=n; return (int)Math.Min(int.MaxValue,total); } public int PeekBlockingPending(ulong owner,ulong stage) { if(!queues.TryGetValue(owner,out var q))return 0; long total=q.Offer!=null||q.Category!=null?1:0;if(q.Receipt!=null)total++; foreach(int count in q.Counts)total+=count; if(q.DeferredStage!=stage)foreach(int count in q.Deferred)total+=count; return (int)Math.Min(int.MaxValue,total); } public int NextTier(ulong owner,ulong stage) { var q=Get(owner);AdvanceDeferred(q,stage); if(q.Receipt!=null)return -1; if(q.Offer!=null)return (int)q.Offer.Tier; if(q.Category!=null)return (int)q.Category.Tier; return Array.FindIndex(q.Counts,x=>x>0); } public CoreCategoryOffer OpenCategoryOffer(ulong owner,ulong run,ulong stage,RunInventory inventory,UpgradeEligibility eligibility) { if(inventory==null||eligibility==null||run==0)return null; var q=Get(owner);AdvanceDeferred(q,stage); if(q.Receipt!=null||q.Offer!=null)return null; if(q.Category!=null)return q.Category.RunId==run&&q.Category.StageId==stage?q.Category:null; if(nextOffer==long.MaxValue)return null; int tier=Array.FindIndex(q.Counts,x=>x>0);if(tier<=0)return null; var options=CoreCategoryRules.EligibleCategories((UpgradeTier)tier,inventory,eligibility).ToList(); var categories=new List();q.Pools.Clear(); while(options.Count>0&&categories.Count<3) { int chosen=random.Next(options.Count);var category=options[chosen];options.RemoveAt(chosen);categories.Add(category); q.Pools[category]=CoreCategoryRules.EligibleIds(category,(UpgradeTier)tier,inventory,eligibility); } q.Counts[tier]--;q.Eligibility=SnapshotEligibility(eligibility); q.Category=new CoreCategoryOffer {RunId=run,StageId=stage,RewardId=checked(++nextOffer),Tier=(UpgradeTier)tier,Categories=categories.ToArray(),Exhausted=categories.Count==0}; return q.Category; } public bool TrySelectCategory(ulong owner,ulong run,long rewardId,CoreCategory category,RunInventory inventory,out CoreAwardReceipt receipt) => TrySelectCategory(owner,run,rewardId,category,inventory,out receipt,out _); public bool TrySelectCategory(ulong owner,ulong run,long rewardId,CoreCategory category,RunInventory inventory,out CoreAwardReceipt receipt,out bool awarded) => TrySelectCategory(owner,run,rewardId,category,inventory,null,out receipt,out awarded); public bool TrySelectCategory(ulong owner,ulong run,long rewardId,CoreCategory category,RunInventory inventory,UpgradeEligibility currentEligibility,out CoreAwardReceipt receipt,out bool awarded) { receipt=null;awarded=false;if(!queues.TryGetValue(owner,out var q))return false; if(q.Receipts.TryGetValue(rewardId,out var cached)) {if(cached.RunId!=run||cached.Category!=category)return false;receipt=cached;return true;} var offer=q.Category; if(offer==null||offer.RunId!=run||offer.RewardId!=rewardId||!offer.Categories.Contains(category))return false; var eligibility=currentEligibility??q.Eligibility; // Preserve the original locked IDs, but never award an effect whose prerequisite or // party/loadout compatibility vanished before this first claim. No IDs are added. foreach(var original in q.Pools.ToArray()) { var compatible=original.Value.Where(id=>CoreCategoryRules.IsEligible(UpgradeCatalog.Get(id),inventory,eligibility)).ToArray(); if(compatible.Length==0)q.Pools.Remove(original.Key);else q.Pools[original.Key]=compatible; } offer.Categories=offer.Categories.Where(x=>q.Pools.ContainsKey(x)).ToArray();offer.Exhausted=offer.Categories.Length==0; if(!q.Pools.TryGetValue(category,out var pool)||pool.Length==0)return false; // Sampling an ID first prevents ability-specific recipients or multiple tags increasing its draw weight. int id=pool[random.Next(pool.Length)],ability=0;string name=null; if(id==(int)UpgradeId.ReserveCell) { var targets=eligibility.ActiveAbilities.Where(x=>x>0&&inventory.Rank(UpgradeId.ReserveCell,x)=0&&index new UpgradeEligibility {ActiveAbilities=(int[])value.ActiveAbilities.Clone(),ActiveNames=(string[])value.ActiveNames.Clone(),DamagingAbilities=(int[])value.DamagingAbilities.Clone(),EchoAbilities=(int[])value.EchoAbilities.Clone(),EchoEligibilitySpecified=value.EchoEligibilitySpecified,MaxHealth=value.MaxHealth,MaxNormalShield=value.MaxNormalShield,ExtraJump=value.ExtraJump,MovementAbility=value.MovementAbility,StatusAbility=value.StatusAbility,MarkAbility=value.MarkAbility,SupportAbility=value.SupportAbility,HasTeammates=value.HasTeammates}; public bool AcknowledgeReceipt(ulong owner,ulong run,long rewardId) { if(!queues.TryGetValue(owner,out var q)||q.Receipt==null||q.Receipt.RunId!=run||q.Receipt.RewardId!=rewardId)return false; q.Receipt=null;return true; } public bool DeferCategoryOffer(ulong owner,ulong run,ulong stage,long rewardId) { if(!queues.TryGetValue(owner,out var q)||q.Category==null||q.Category.RunId!=run||q.Category.StageId!=stage||q.Category.RewardId!=rewardId||!q.Category.Exhausted)return false; int tier=(int)q.Category.Tier;if(q.Deferred[tier]x>0); if(tier<0)return null; q.Counts[tier]--; var cards=new List(); var options=new List(); for(int t=tier;t>=0 && cards.Count<3;t--) { options.Clear(); foreach(var d in UpgradeCatalog.All.Where(x=>(int)x.Tier==t)) { if(d.Id==(int)UpgradeId.CrimsonCascade && inventory.Rank(UpgradeId.SerratedPayload)==0)continue; if(d.Id==(int)UpgradeId.EchoCast && eligibility.DamagingAbilities.Length==0)continue; if(d.Id==(int)UpgradeId.ReserveCell) { for(int i=0;i0 && cards.Count<3) { int choice=random.Next(options.Count);cards.Add(options[choice]);options.RemoveAt(choice); } } foreach(var card in cards)if((int)card.Tier list,UpgradeDefinition d,RunInventory inventory,int ability,string abilityName,UpgradeEligibility eligibility) { long rank=inventory.Rank((UpgradeId)d.Id,ability); // The last representable rank cannot accept another copy. This is a storage boundary, // not an authored stacking cap; lower ranks remain eligible, including the final copy. if(rank==long.MaxValue)return; list.Add(new UpgradeCard { UpgradeId=d.Id,AbilityId=ability,Name=d.Name+(ability>0?" ยท "+abilityName:""),Tier=d.Tier,CurrentRank=rank,MaxRank=d.MaxRank,Description=UpgradeEffectResolver.Describe((UpgradeId)d.Id,rank,eligibility.MaxHealth,eligibility.MaxNormalShield),Restriction=d.Restriction }); } public bool Select(ulong owner,long rewardId,int upgradeId,int ability,RunInventory inventory) { var q=Get(owner);if(q.Offer==null||q.Offer.RewardId!=rewardId||!q.Offer.Cards.Any(x=>x.UpgradeId==upgradeId&&x.AbilityId==ability))return false; if(!inventory.Add(upgradeId,ability))return false; q.Offer=null;return true; } public bool Dismiss(ulong owner,long rewardId) {var q=Get(owner);if(q.Offer==null||q.Offer.RewardId!=rewardId||!q.Offer.Exhausted)return false;q.Offer=null;return true;} public void Remove(ulong owner)=>queues.Remove(owner); public void Clear(){queues.Clear();deaths.Clear();drought=0;} // Preserve process-local reward identity across runs. private Queue Get(ulong owner){if(!queues.TryGetValue(owner,out var q)){q=new Queue();queues.Add(owner,q);}return q;} } }