using Microsoft.EntityFrameworkCore; using SVSim.Database; using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Services; using SVSim.EmulatedEntrypoint.Models.Dtos; namespace SVSim.EmulatedEntrypoint.Services; public class CardAcquisitionService : ICardAcquisitionService { private readonly SVSimDbContext _db; private readonly RewardGrantService _rewards; public CardAcquisitionService(SVSimDbContext db, RewardGrantService rewards) { _db = db; _rewards = rewards; } public async Task GrantManyAsync(long viewerId, IEnumerable newCardIds) { var viewer = await LoadViewerWithGraph(viewerId); var rewardList = new List(); // Bucket the input by id so multi-copy grants increment count once but cascade fires once. foreach (var grp in newCardIds.GroupBy(id => id)) { int count = grp.Count(); var granted = await _rewards.ApplyAsync(viewer, UserGoodsType.Card, grp.Key, count); foreach (var g in granted) { rewardList.Add(new RewardListEntry { RewardType = g.RewardType, RewardId = g.RewardId, RewardNum = g.RewardNum, }); } } await _db.SaveChangesAsync(); return new CardGrantResult(rewardList); } public async Task BackfillCosmeticsAsync(long viewerId) { var viewer = await LoadViewerWithGraph(viewerId); var rewardList = new List(); // Foil resolution: cascade rows live on non-foil ids. Apply the +1 convention. var lookupCardIds = viewer.Cards .Select(c => c.Card.IsFoil ? c.Card.Id - 1 : c.Card.Id) .Distinct() .ToList(); var cascade = await _db.CardCosmeticRewards .Where(r => lookupCardIds.Contains(r.CardId)) .ToListAsync(); foreach (var reward in cascade) { // Skip if the viewer already owns this cosmetic. ApplyAsync's cosmetic branches // unconditionally return a wire entry (top-level grant semantics), so we must // filter at the caller side to avoid emitting "+0 received" lines for cosmetics // the viewer has owned for ages. if (AlreadyOwnsCosmetic(viewer, reward.Type, reward.CosmeticId)) continue; var goodsType = (UserGoodsType)(int)reward.Type; var granted = await _rewards.ApplyAsync(viewer, goodsType, reward.CosmeticId, 1); foreach (var g in granted) { rewardList.Add(new RewardListEntry { RewardType = g.RewardType, RewardId = g.RewardId, RewardNum = g.RewardNum, }); } } await _db.SaveChangesAsync(); return new CardGrantResult(rewardList); } private static bool AlreadyOwnsCosmetic(Viewer viewer, CosmeticType type, long id) => type switch { CosmeticType.Sleeve => viewer.Sleeves.Any(s => s.Id == id), CosmeticType.Emblem => viewer.Emblems.Any(e => e.Id == id), CosmeticType.Skin => viewer.LeaderSkins.Any(s => s.Id == id), CosmeticType.Degree => viewer.Degrees.Any(d => d.Id == id), CosmeticType.MyPageBG => viewer.MyPageBackgrounds.Any(b => b.Id == id), _ => false, }; private Task LoadViewerWithGraph(long viewerId) => _db.Viewers .Include(v => v.Cards).ThenInclude(c => c.Card) .Include(v => v.LeaderSkins) .Include(v => v.Sleeves) .Include(v => v.Emblems) .Include(v => v.Degrees) .Include(v => v.MyPageBackgrounds) .AsSplitQuery() .FirstAsync(v => v.Id == viewerId); }