fix(pack): include all pack legendaries in gacha-point catalog + correct class_id
This commit is contained in:
@@ -41,6 +41,13 @@ public sealed class GachaPointService : IGachaPointService
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
// Re-query legendaries with Class loaded — pool provider doesn't include navs,
|
||||
// so card.Class is null on every pool entry and class_id would collapse to "0".
|
||||
var legendariesWithClass = await _db.Cards
|
||||
.Where(c => legendaryCardIds.Contains(c.Id))
|
||||
.Include(c => c.Class)
|
||||
.ToListAsync();
|
||||
|
||||
// Pull both cosmetic types in one trip. Group by card_id for O(1) lookup below.
|
||||
var cosmeticsByCard = await _db.CardCosmeticRewards
|
||||
.Where(r => legendaryCardIds.Contains(r.CardId)
|
||||
@@ -53,56 +60,56 @@ public sealed class GachaPointService : IGachaPointService
|
||||
var standard = new List<GachaPointRewardDto>();
|
||||
var leader = new List<GachaPointRewardDto>();
|
||||
|
||||
foreach (var card in pool
|
||||
.Where(c => c.Rarity == Rarity.Legendary && !c.IsFoil)
|
||||
foreach (var card in legendariesWithClass
|
||||
// Neutral cards have Class=null; client wire-encodes them as class_id="0".
|
||||
.OrderBy(c => c.Class?.Id ?? 0).ThenBy(c => c.Id))
|
||||
{
|
||||
if (!cosmeticLookup.TryGetValue(card.Id, out var cosmetics)) continue;
|
||||
var emblems = cosmetics.Where(c => c.Type == CosmeticType.Emblem).ToList();
|
||||
var skin = cosmetics.FirstOrDefault(c => c.Type == CosmeticType.Skin);
|
||||
if (emblems.Count == 0) continue; // every gacha-point entry has at least one emblem
|
||||
cosmeticLookup.TryGetValue(card.Id, out var cosmetics);
|
||||
var emblems = cosmetics?.Where(c => c.Type == CosmeticType.Emblem).ToList()
|
||||
?? new List<CardCosmeticReward>();
|
||||
var skin = cosmetics?.FirstOrDefault(c => c.Type == CosmeticType.Skin);
|
||||
|
||||
var classId = (card.Class?.Id ?? 0).ToString(CultureInfo.InvariantCulture);
|
||||
var isReceived = receivedCardIds.Contains(card.Id);
|
||||
|
||||
if (IsLeaderCard(skin))
|
||||
{
|
||||
// Leader card — 3 entries in capture order: Sleeve/Card-cosmetic (type 6),
|
||||
// Skin (type 10), Emblem (type 7). The reward_type=6 entry's detail id is the
|
||||
// card_id itself, mirroring the prod capture exactly (no Sleeve cosmetic row
|
||||
// is required — synthesizing from card.Id is robust to missing rows). For the
|
||||
// emblem we take the first row only; no capture has shown leader cards with
|
||||
// multiple emblems, and the verified shape is exactly 1 Sleeve + 1 Skin + 1
|
||||
// Emblem. Revisit if such a capture lands.
|
||||
var emblem = emblems[0];
|
||||
// Leader card — 2 or 3 entries: Sleeve/Card-cosmetic (type 6) with detail=card_id,
|
||||
// Skin (type 10) with detail=leader_skin_id, and an Emblem (type 7) per emblem row.
|
||||
// Most leader cards in captured packs have exactly 1 emblem, but we emit per-emblem
|
||||
// for consistency with the standard-legendary branch.
|
||||
var rewardList = new List<GachaPointRewardDetailEntry>
|
||||
{
|
||||
new GachaPointRewardDetailEntry
|
||||
{
|
||||
RewardType = (int)UserGoodsType.Sleeve, RewardDetailId = card.Id, RewardNumber = 1,
|
||||
},
|
||||
new GachaPointRewardDetailEntry
|
||||
{
|
||||
RewardType = (int)UserGoodsType.Skin,
|
||||
RewardDetailId = skin!.CosmeticId, RewardNumber = 1,
|
||||
},
|
||||
};
|
||||
foreach (var emblem in emblems)
|
||||
{
|
||||
rewardList.Add(new GachaPointRewardDetailEntry
|
||||
{
|
||||
RewardType = (int)UserGoodsType.Emblem,
|
||||
RewardDetailId = emblem.CosmeticId, RewardNumber = 1,
|
||||
});
|
||||
}
|
||||
leader.Add(new GachaPointRewardDto
|
||||
{
|
||||
ClassId = classId, CardId = card.Id, IsReceived = isReceived,
|
||||
RewardList =
|
||||
{
|
||||
new GachaPointRewardDetailEntry
|
||||
{
|
||||
RewardType = (int)UserGoodsType.Sleeve, RewardDetailId = card.Id, RewardNumber = 1,
|
||||
},
|
||||
new GachaPointRewardDetailEntry
|
||||
{
|
||||
RewardType = (int)UserGoodsType.Skin,
|
||||
RewardDetailId = skin!.CosmeticId, RewardNumber = 1,
|
||||
},
|
||||
new GachaPointRewardDetailEntry
|
||||
{
|
||||
RewardType = (int)UserGoodsType.Emblem,
|
||||
RewardDetailId = emblem.CosmeticId, RewardNumber = 1,
|
||||
},
|
||||
},
|
||||
RewardList = rewardList,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard legendaries can have multiple emblem cosmetics (e.g. prod capture
|
||||
// pack 10008 card 108044010 has emblem ids 900041040 and 900041050). Emit one
|
||||
// reward_list entry per emblem.
|
||||
// Standard legendary — one reward_list entry per emblem cosmetic (possibly zero
|
||||
// entries for packs whose emblem mappings weren't in the capture sweep, e.g. pack
|
||||
// 10001 Classic). The card is still grantable; the exchange's cosmetic cascade
|
||||
// delivers whatever rows actually exist in CardCosmeticRewards.
|
||||
var dto = new GachaPointRewardDto
|
||||
{
|
||||
ClassId = classId, CardId = card.Id, IsReceived = isReceived,
|
||||
|
||||
@@ -478,6 +478,93 @@ public class GachaPointServiceTests
|
||||
Is.True, "card grant missing");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetRewards_emits_correct_class_id_for_non_neutral_cards()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
// Use the pre-seeded Forestcraft (Id=1) and Havencraft (Id=7) classes from ReferenceDataImporter.
|
||||
var forest = await db.Classes.FirstAsync(c => c.Id == 1);
|
||||
var haven = await db.Classes.FirstAsync(c => c.Id == 7);
|
||||
|
||||
var set = new ShadowverseCardSetEntry { Id = 10008, IsInRotation = true };
|
||||
db.CardSets.Add(set);
|
||||
set.Cards.AddRange(new[]
|
||||
{
|
||||
new ShadowverseCardEntry
|
||||
{
|
||||
Id = 108141010, Name = "leg-forest", Rarity = Rarity.Legendary,
|
||||
Class = forest, IsFoil = false,
|
||||
},
|
||||
new ShadowverseCardEntry
|
||||
{
|
||||
Id = 108741010, Name = "leg-haven", Rarity = Rarity.Legendary,
|
||||
Class = haven, IsFoil = false,
|
||||
},
|
||||
});
|
||||
db.CardCosmeticRewards.AddRange(
|
||||
new CardCosmeticReward { CardId = 108141010, Type = CosmeticType.Emblem, CosmeticId = 1081410100 },
|
||||
new CardCosmeticReward { CardId = 108741010, Type = CosmeticType.Emblem, CosmeticId = 1087410100 });
|
||||
db.Packs.Add(new PackConfigEntry
|
||||
{
|
||||
Id = 10008, BasePackId = 10008, PackCategory = PackCategory.LegendCardPack,
|
||||
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
|
||||
GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 },
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGachaPointService>();
|
||||
var result = await svc.GetRewardsAsync(10008, viewerId);
|
||||
|
||||
// Cards must surface with their REAL class id, not the "0" neutral fallback caused
|
||||
// by the pool provider not Including Class.
|
||||
Assert.That(result, Has.Count.EqualTo(2));
|
||||
var forestEntry = result.Single(r => r.CardId == 108141010);
|
||||
var havenEntry = result.Single(r => r.CardId == 108741010);
|
||||
Assert.That(forestEntry.ClassId, Is.EqualTo("1"));
|
||||
Assert.That(havenEntry.ClassId, Is.EqualTo("7"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetRewards_includes_legendaries_without_emblem_cosmetics()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
// Pack 10001 simulation: a legendary with NO CardCosmeticReward rows at all.
|
||||
// Must still appear in the catalog (per user spec — all pack legendaries are
|
||||
// exchangeable, regardless of whether we have captured emblem mappings).
|
||||
// Use a unique card-set id (10099) because SVSimTestFactory already seeds a
|
||||
// minimal CardSet at Id=10001.
|
||||
var set = new ShadowverseCardSetEntry { Id = 10099, IsInRotation = true };
|
||||
db.CardSets.Add(set);
|
||||
set.Cards.Add(new ShadowverseCardEntry
|
||||
{
|
||||
Id = 101141020, Name = "old-leg", Rarity = Rarity.Legendary,
|
||||
Class = null, IsFoil = false,
|
||||
});
|
||||
db.Packs.Add(new PackConfigEntry
|
||||
{
|
||||
Id = 10099, BasePackId = 10099, PackCategory = PackCategory.LegendCardPack,
|
||||
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
|
||||
GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 },
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGachaPointService>();
|
||||
var result = await svc.GetRewardsAsync(10099, viewerId);
|
||||
|
||||
Assert.That(result, Has.Count.EqualTo(1));
|
||||
Assert.That(result[0].CardId, Is.EqualTo(101141020));
|
||||
Assert.That(result[0].RewardList, Is.Empty,
|
||||
"legendary without emblem rows emits empty reward_list — the catalog declaration is just the card itself");
|
||||
}
|
||||
|
||||
private static void SeedPackWithOneLegendary(SVSimDbContext db, int packId, int threshold)
|
||||
{
|
||||
var cls = db.Classes.Find(0) ?? new ClassEntry { Id = 0, Name = "Neutral" };
|
||||
|
||||
Reference in New Issue
Block a user