From 739f6299962078455f78a684f06031f61d5c6c2c Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 00:07:28 -0400 Subject: [PATCH] feat(pack): accrue gacha points on /pack/open (skip tutorial) --- .../Controllers/PackController.cs | 8 ++ .../Controllers/PackControllerOpenTests.cs | 121 ++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/SVSim.EmulatedEntrypoint/Controllers/PackController.cs b/SVSim.EmulatedEntrypoint/Controllers/PackController.cs index 3241981..2274f64 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/PackController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/PackController.cs @@ -246,6 +246,7 @@ public class PackController : SVSimController var viewer = await _db.Viewers .Include(v => v.PackOpenCounts) + .Include(v => v.GachaPointBalances) .Include(v => v.MissionData) .Include(v => v.Items).ThenInclude(i => i.Item) .AsSplitQuery() @@ -318,6 +319,13 @@ public class PackController : SVSimController var draw = _opener.Draw(pack, _pools, drawCount, request.ExcludeCardIds, _rng); var grant = await _acquisition.GrantManyAsync(viewerId, draw.Cards.Select(c => c.CardId)); + // Accrue gacha points (skip tutorial path — the starter pack isn't a real open). + if (!isTutorialPath) + { + _gachaPoint.Accrue(viewer, pack, child, drawCount); + await _db.SaveChangesAsync(); + } + // Build reward_list. The service produces the type=5 (Card) entries with post-state counts // plus any cosmetic grants. Currency entry (type=2 Crystals or type=9 Rupy) stays in the // controller — it's a pack-purchase concern, not a card-grant concern. The client's diff --git a/SVSim.UnitTests/Controllers/PackControllerOpenTests.cs b/SVSim.UnitTests/Controllers/PackControllerOpenTests.cs index 26950e7..5c2f2b2 100644 --- a/SVSim.UnitTests/Controllers/PackControllerOpenTests.cs +++ b/SVSim.UnitTests/Controllers/PackControllerOpenTests.cs @@ -464,4 +464,125 @@ public class PackControllerOpenTests }); await db.SaveChangesAsync(); } + + [Test] + public async Task PackOpen_accrues_gacha_points_per_pack_drawn() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Classes.Add(new ClassEntry { Id = 0, Name = "Neutral" }); + var set = new ShadowverseCardSetEntry { Id = 10008, IsInRotation = true }; + db.CardSets.Add(set); + // Need a pool that can draw 8 cards across the 4 default rarities. + for (int i = 0; i < 30; i++) + { + set.Cards.Add(new ShadowverseCardEntry + { + Id = 10804_1010 + i, + Name = $"c{i}", + Rarity = (Rarity)((i % 4) + 1), // Bronze..Legendary + Class = db.Classes.Local.First(), + IsFoil = false, + }); + } + db.Packs.Add(new PackConfigEntry + { + Id = 10008, BasePackId = 10008, PackCategory = PackCategory.LegendCardPack, + CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30), + GachaType = 1, + GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 }, + ChildGachas = + { + new PackChildGachaEntry + { + GachaId = 100087, TypeDetail = 7, Cost = 100, CardCount = 8, + OverrideIncreaseGachaPoint = 0, + }, + }, + }); + var viewer = await db.Viewers.FirstAsync(v => v.Id == viewerId); + viewer.Currency.Rupees = 10000; + await db.SaveChangesAsync(); + } + + using var client = factory.CreateAuthenticatedClient(viewerId); + var body = new StringContent( + """{"parent_gacha_id":10008,"gacha_id":100087,"gacha_type":1,"pack_number":3,"exclude_card_ids":[],"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""", + System.Text.Encoding.UTF8, "application/json"); + var response = await client.PostAsync("/pack/open", body); + var text = await response.Content.ReadAsStringAsync(); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), text); + + using var scope2 = factory.Services.CreateScope(); + var db2 = scope2.ServiceProvider.GetRequiredService(); + var v = await db2.Viewers + .Include(x => x.GachaPointBalances) + .FirstAsync(x => x.Id == viewerId); + Assert.That(v.GachaPointBalances.Single().Points, Is.EqualTo(3)); + } + + [Test] + public async Task TutorialPackOpen_does_not_accrue_gacha_points() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + // Seed the starter pack 99047 with a GachaPointConfig set — the tutorial-path skip + // must hold even when the pack is technically point-eligible. + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Classes.Add(new ClassEntry { Id = 0, Name = "Neutral" }); + var set = new ShadowverseCardSetEntry { Id = 99047, IsInRotation = true }; + db.CardSets.Add(set); + for (int i = 0; i < 30; i++) + { + set.Cards.Add(new ShadowverseCardEntry + { + Id = 99047_1010 + i, Name = $"c{i}", + Rarity = (Rarity)((i % 4) + 1), + Class = db.Classes.Local.First(), IsFoil = false, + }); + } + db.Items.Add(new ItemEntry { Id = 90001, Name = "starter-ticket" }); + db.Packs.Add(new PackConfigEntry + { + Id = 99047, BasePackId = 99047, PackCategory = PackCategory.LegendCardPack, + CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30), + GachaType = 1, + GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 }, + ChildGachas = + { + new PackChildGachaEntry + { + GachaId = 990475, TypeDetail = 5, Cost = 0, CardCount = 8, + ItemId = 90001, + }, + }, + }); + var viewer = await db.Viewers + .Include(v => v.Items).ThenInclude(i => i.Item) + .FirstAsync(v => v.Id == viewerId); + viewer.Items.Add(new OwnedItemEntry { Item = db.Items.Local.First(), Count = 1, Viewer = viewer }); + viewer.MissionData.TutorialState = 41; // pre-END so the tutorial path is allowed + await db.SaveChangesAsync(); + } + + using var client = factory.CreateAuthenticatedClient(viewerId); + var body = new StringContent( + """{"parent_gacha_id":99047,"gacha_id":990475,"gacha_type":1,"pack_number":1,"exclude_card_ids":[],"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""", + System.Text.Encoding.UTF8, "application/json"); + var response = await client.PostAsync("/tutorial/pack_open", body); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), + await response.Content.ReadAsStringAsync()); + + using var scope2 = factory.Services.CreateScope(); + var db2 = scope2.ServiceProvider.GetRequiredService(); + var v = await db2.Viewers + .Include(x => x.GachaPointBalances) + .FirstAsync(x => x.Id == viewerId); + Assert.That(v.GachaPointBalances, Is.Empty, "tutorial path must not accrue gacha points"); + } }