From 168e347a82078df5e2a310f9c99106892115cb81 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 00:16:08 -0400 Subject: [PATCH] feat(pack): wire real gacha-point balance into /pack/info (skip ticket-only packs) --- .../Controllers/PackController.cs | 43 +++++++--- .../Controllers/PackControllerInfoTests.cs | 80 +++++++++++++++++++ 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Controllers/PackController.cs b/SVSim.EmulatedEntrypoint/Controllers/PackController.cs index 2274f64..98f7984 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/PackController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/PackController.cs @@ -80,18 +80,48 @@ public class PackController : SVSimController .Select(i => new { ItemId = (long)EF.Property(i, "ItemId"), i.Count }) .ToDictionaryAsync(x => x.ItemId, x => x.Count); + var gachaPointBalancesByPackId = await _db.Viewers + .Where(v => v.Id == viewerId) + .SelectMany(v => v.GachaPointBalances) + .Select(b => new { b.PackId, b.Points }) + .ToDictionaryAsync(x => x.PackId, x => x.Points); + return new PackInfoResponse { - PackConfigList = packs.Select(p => ToDto(p, openCounts, ownedItemsByItemId)).ToList(), + PackConfigList = packs + .Select(p => ToDto(p, openCounts, ownedItemsByItemId, gachaPointBalancesByPackId)) + .ToList(), }; } private static PackConfigDto ToDto( PackConfigEntry p, IReadOnlyDictionary openCounts, - IReadOnlyDictionary ownedItemsByItemId) + IReadOnlyDictionary ownedItemsByItemId, + IReadOnlyDictionary gachaPointBalancesByPackId) { int openCount = openCounts.TryGetValue(p.Id, out var oc) ? oc.OpenCount : 0; + + // Ticket-only pack: every child is TICKET (4) or TICKET_MULTI (5). These are + // gifted-currency packs (tutorial starter, throwback) that don't participate in + // gacha-point accrual or exchange, even if GachaPointConfig is set in seed. + bool isTicketOnly = p.ChildGachas.All(c => c.TypeDetail == 4 || c.TypeDetail == 5); + + PackGachaPointDto? gachaPointDto = null; + if (p.GachaPointConfig is not null && !isTicketOnly) + { + int balance = gachaPointBalancesByPackId.TryGetValue(p.Id, out var b) ? b : 0; + int threshold = p.GachaPointConfig.ExchangeablePoint; + gachaPointDto = new PackGachaPointDto + { + PackId = p.BasePackId.ToString(CultureInfo.InvariantCulture), + GachaPoint = balance, + IncreaseGachaPoint = p.GachaPointConfig.IncreaseGachaPoint.ToString(CultureInfo.InvariantCulture), + ExchangeableGachaPoint = threshold, + IsExchangeableGachaPoint = balance >= threshold, + }; + } + return new PackConfigDto { ParentGachaId = p.Id, @@ -133,14 +163,7 @@ public class PackController : SVSimController OpenCountLimit = p.OpenCountLimit, IsHide = p.IsHide ? 1 : 0, PackCategory = (int)p.PackCategory, - GachaPoint = p.GachaPointConfig is null ? null : new PackGachaPointDto - { - PackId = p.BasePackId.ToString(CultureInfo.InvariantCulture), - GachaPoint = 0, - IncreaseGachaPoint = p.GachaPointConfig.IncreaseGachaPoint.ToString(CultureInfo.InvariantCulture), - ExchangeableGachaPoint = p.GachaPointConfig.ExchangeablePoint, - IsExchangeableGachaPoint = false, - }, + GachaPoint = gachaPointDto, IsPreRelease = p.IsPreRelease, ExistsPurchaseReward = false, IsNew = p.IsNew, diff --git a/SVSim.UnitTests/Controllers/PackControllerInfoTests.cs b/SVSim.UnitTests/Controllers/PackControllerInfoTests.cs index be3b394..ae67eb2 100644 --- a/SVSim.UnitTests/Controllers/PackControllerInfoTests.cs +++ b/SVSim.UnitTests/Controllers/PackControllerInfoTests.cs @@ -130,4 +130,84 @@ public class PackControllerInfoTests Assert.That(gachaPoint.ValueKind, Is.EqualTo(JsonValueKind.Null), "gacha_point should serialize as explicit null when no GachaPointConfig is set."); } + + [Test] + public async Task Info_projects_viewer_gacha_point_balance_into_gacha_point_block() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Packs.Add(new PackConfigEntry + { + Id = 10008, BasePackId = 10008, PackCategory = PackCategory.LegendCardPack, + CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30), + GachaType = 1, GachaDetail = "test", + GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 }, + ChildGachas = + { + // Must include at least one non-ticket child so this pack is NOT ticket-only + // and remains visible with a gacha_point block. + new PackChildGachaEntry { GachaId = 100087, TypeDetail = 7, Cost = 100, CardCount = 8 }, + }, + }); + var viewer = await db.Viewers + .Include(v => v.GachaPointBalances) + .FirstAsync(v => v.Id == viewerId); + viewer.GachaPointBalances.Add(new ViewerGachaPointBalance { PackId = 10008, Points = 450 }); + await db.SaveChangesAsync(); + } + + using var client = factory.CreateAuthenticatedClient(viewerId); + var response = await client.PostAsync("/pack/info", JsonBody(EmptyEnvelope)); + var text = await response.Content.ReadAsStringAsync(); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), text); + + using var doc = JsonDocument.Parse(text); + var pack = doc.RootElement.GetProperty("pack_config_list")[0]; + var gp = pack.GetProperty("gacha_point"); + Assert.That(gp.GetProperty("gacha_point").GetInt32(), Is.EqualTo(450)); + Assert.That(gp.GetProperty("is_exchangeable_gacha_point").GetBoolean(), Is.True, + "balance >= threshold should flip the gate"); + } + + [Test] + public async Task Info_omits_gacha_point_block_for_ticket_only_packs() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + // Even though the pack has a GachaPointConfig, it must be hidden because every + // child is a ticket type (4 or 5). Mirrors prod for starter pack 99047. + db.Packs.Add(new PackConfigEntry + { + Id = 99047, BasePackId = 99047, PackCategory = PackCategory.LegendCardPack, + CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30), + GachaType = 1, GachaDetail = "test", + GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 }, + ChildGachas = + { + new PackChildGachaEntry { GachaId = 990475, TypeDetail = 5, Cost = 0, CardCount = 8 }, + }, + }); + await db.SaveChangesAsync(); + } + + using var client = factory.CreateAuthenticatedClient(viewerId); + var response = await client.PostAsync("/pack/info", JsonBody(EmptyEnvelope)); + var text = await response.Content.ReadAsStringAsync(); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), text); + + using var doc = JsonDocument.Parse(text); + var pack = doc.RootElement.GetProperty("pack_config_list")[0]; + // Either the key is absent (WhenWritingNull dropped it) or the value is null. + if (pack.TryGetProperty("gacha_point", out var gp)) + { + Assert.That(gp.ValueKind, Is.EqualTo(JsonValueKind.Null), + "ticket-only pack must not emit a gacha_point block"); + } + } }