102 lines
3.8 KiB
C#
102 lines
3.8 KiB
C#
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<CardGrantResult> GrantManyAsync(long viewerId, IEnumerable<long> newCardIds)
|
|
{
|
|
var viewer = await LoadViewerWithGraph(viewerId);
|
|
var rewardList = new List<RewardListEntry>();
|
|
|
|
// 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<CardGrantResult> BackfillCosmeticsAsync(long viewerId)
|
|
{
|
|
var viewer = await LoadViewerWithGraph(viewerId);
|
|
var rewardList = new List<RewardListEntry>();
|
|
|
|
// 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<Viewer> 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);
|
|
}
|