diff --git a/SVSim.EmulatedEntrypoint/Services/GachaPointService.cs b/SVSim.EmulatedEntrypoint/Services/GachaPointService.cs
index a8a07df..89975c9 100644
--- a/SVSim.EmulatedEntrypoint/Services/GachaPointService.cs
+++ b/SVSim.EmulatedEntrypoint/Services/GachaPointService.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using Microsoft.EntityFrameworkCore;
using SVSim.Database;
using SVSim.Database.Enums;
@@ -58,11 +59,11 @@ public sealed class GachaPointService : IGachaPointService
.OrderBy(c => c.Class?.Id ?? 0).ThenBy(c => c.Id))
{
if (!cosmeticLookup.TryGetValue(card.Id, out var cosmetics)) continue;
- var emblem = cosmetics.FirstOrDefault(c => c.Type == CosmeticType.Emblem);
+ var emblems = cosmetics.Where(c => c.Type == CosmeticType.Emblem).ToList();
var skin = cosmetics.FirstOrDefault(c => c.Type == CosmeticType.Skin);
- if (emblem is null) continue; // every gacha-point entry has an emblem
+ if (emblems.Count == 0) continue; // every gacha-point entry has at least one emblem
- var classId = (card.Class?.Id ?? 0).ToString();
+ var classId = (card.Class?.Id ?? 0).ToString(CultureInfo.InvariantCulture);
var isReceived = receivedCardIds.Contains(card.Id);
if (IsLeaderCard(skin))
@@ -70,7 +71,11 @@ public sealed class GachaPointService : IGachaPointService
// 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).
+ // 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.Add(new GachaPointRewardDto
{
ClassId = classId, CardId = card.Id, IsReceived = isReceived,
@@ -95,19 +100,23 @@ public sealed class GachaPointService : IGachaPointService
}
else
{
- standard.Add(new GachaPointRewardDto
+ // 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.
+ var dto = new GachaPointRewardDto
{
ClassId = classId, CardId = card.Id, IsReceived = isReceived,
- RewardList =
+ };
+ foreach (var emblem in emblems)
+ {
+ dto.RewardList.Add(new GachaPointRewardDetailEntry
{
- new GachaPointRewardDetailEntry
- {
- RewardType = (int)UserGoodsType.Emblem,
- RewardDetailId = emblem.CosmeticId,
- RewardNumber = 1,
- },
- },
- });
+ RewardType = (int)UserGoodsType.Emblem,
+ RewardDetailId = emblem.CosmeticId,
+ RewardNumber = 1,
+ });
+ }
+ standard.Add(dto);
}
}
diff --git a/SVSim.EmulatedEntrypoint/Services/IGachaPointService.cs b/SVSim.EmulatedEntrypoint/Services/IGachaPointService.cs
index b5681cf..7d89b5a 100644
--- a/SVSim.EmulatedEntrypoint/Services/IGachaPointService.cs
+++ b/SVSim.EmulatedEntrypoint/Services/IGachaPointService.cs
@@ -16,7 +16,7 @@ public interface IGachaPointService
///
/// Increment the viewer's balance for by
- /// child.OverrideIncreaseGachaPoint ?? pack.GachaPointConfig.IncreaseGachaPoint
+ /// child.OverrideIncreaseGachaPoint > 0 ? child.OverrideIncreaseGachaPoint : pack.GachaPointConfig.IncreaseGachaPoint
/// times . No-op when the pack lacks a GachaPointConfig.
/// Caller is responsible for SaveChangesAsync.
///
diff --git a/SVSim.UnitTests/Services/GachaPointServiceTests.cs b/SVSim.UnitTests/Services/GachaPointServiceTests.cs
index c545a55..0434cb8 100644
--- a/SVSim.UnitTests/Services/GachaPointServiceTests.cs
+++ b/SVSim.UnitTests/Services/GachaPointServiceTests.cs
@@ -144,6 +144,49 @@ public class GachaPointServiceTests
Assert.That(leaderEntry.RewardList[2].RewardDetailId, Is.EqualTo(704141010));
}
+ [Test]
+ public async Task GetRewards_emits_one_entry_per_emblem_for_cards_with_multiple_emblems()
+ {
+ using var factory = new SVSimTestFactory();
+ long viewerId = await factory.SeedViewerAsync();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ var set = new ShadowverseCardSetEntry { Id = 10008, IsInRotation = true };
+ db.CardSets.Add(set);
+ var leg = new ShadowverseCardEntry
+ {
+ Id = 108044010, Name = "leg-multi-emblem", Rarity = Rarity.Legendary,
+ Class = null, IsFoil = false,
+ };
+ set.Cards.Add(leg);
+
+ // Two emblems for one card — matches prod capture pack 10008 card 108044010.
+ db.CardCosmeticRewards.AddRange(
+ new CardCosmeticReward { CardId = 108044010, Type = CosmeticType.Emblem, CosmeticId = 900041040 },
+ new CardCosmeticReward { CardId = 108044010, Type = CosmeticType.Emblem, CosmeticId = 900041050 });
+
+ 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();
+ var result = await svc.GetRewardsAsync(10008, viewerId);
+
+ Assert.That(result, Has.Count.EqualTo(1));
+ var entry = result[0];
+ Assert.That(entry.CardId, Is.EqualTo(108044010));
+ Assert.That(entry.RewardList, Has.Count.EqualTo(2),
+ "cards with multiple emblems must emit one reward_list entry per emblem");
+ var detailIds = entry.RewardList.Select(r => r.RewardDetailId).OrderBy(x => x).ToList();
+ Assert.That(detailIds, Is.EqualTo(new[] { 900041040L, 900041050L }));
+ Assert.That(entry.RewardList.All(r => r.RewardType == 7), Is.True);
+ }
+
[Test]
public async Task GetRewards_marks_already_received_cards()
{