diff --git a/SVSim.EmulatedEntrypoint/Services/StoryService.cs b/SVSim.EmulatedEntrypoint/Services/StoryService.cs index e2c2059..fefad98 100644 --- a/SVSim.EmulatedEntrypoint/Services/StoryService.cs +++ b/SVSim.EmulatedEntrypoint/Services/StoryService.cs @@ -1,12 +1,16 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using System.Text.Json; using SVSim.Database; using SVSim.Database.Entities.Story; using SVSim.Database.Enums; using SVSim.Database.Models.Config; using SVSim.Database.Repositories.Deck; +using SVSim.Database.Repositories.BuildDeck; using SVSim.Database.Services; using SVSim.Database.Repositories.Story; +using SVSim.EmulatedEntrypoint.Models.Dtos; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; using SVSim.EmulatedEntrypoint.Models.Dtos.Story; namespace SVSim.EmulatedEntrypoint.Services; @@ -19,6 +23,7 @@ public class StoryService : IStoryService private readonly SVSimDbContext _db; private readonly IGameConfigService _configService; private readonly IDeckRepository _deckRepository; + private readonly IBuildDeckRepository _buildDecks; private readonly ILogger _logger; public StoryService( @@ -28,6 +33,7 @@ public class StoryService : IStoryService SVSimDbContext db, IGameConfigService configService, IDeckRepository deckRepository, + IBuildDeckRepository buildDecks, ILogger logger) { _master = master; @@ -36,6 +42,7 @@ public class StoryService : IStoryService _db = db; _configService = configService; _deckRepository = deckRepository; + _buildDecks = buildDecks; _logger = logger; } @@ -344,16 +351,85 @@ public class StoryService : IStoryService { var byFormat = await _deckRepository.GetDecksByFormats( viewerId, new[] { SVSim.Database.Enums.Format.Rotation, SVSim.Database.Enums.Format.Unlimited }); - return new GetDeckListResponse + + var resp = new GetDeckListResponse { UserDeckRotation = byFormat[SVSim.Database.Enums.Format.Rotation] - .Select(d => new SVSim.EmulatedEntrypoint.Models.Dtos.UserDeck(d)).ToList(), + .Select(d => new UserDeck(d)).ToList(), UserDeckUnlimited = byFormat[SVSim.Database.Enums.Format.Unlimited] - .Select(d => new SVSim.EmulatedEntrypoint.Models.Dtos.UserDeck(d)).ToList(), - BuildDeckList = new List(), // v1: empty + .Select(d => new UserDeck(d)).ToList(), MaintenanceCardList = new List(), }; + + // The chapter's leader (CharaId == class_id 1-8 for standard classes) drives which + // prebuilt/trial decks the story deck-select shows. Non-class chapters (custom leaders, + // chara_id outside 1-8) get empty build/trial lists, matching prod. + var chapter = await _master.GetChapterByIdAsync(storyId); + int classId = chapter?.CharaId ?? 0; + if (classId is >= 1 and <= 8) + { + var storyDecks = await _buildDecks.GetStoryDecksByClass(classId); + resp.BuildDeckList = storyDecks + .Where(d => d.Kind == StoryDeckKind.Build) + .Select(ToBuildDeck).ToList(); + resp.TrialDeckList = storyDecks + .Where(d => d.Kind == StoryDeckKind.Trial) + .Select(ToTrialDeck).ToList(); + } + + // default_deck_list — all 8 starter decks, keyed by deck_no string (same shape as /deck/info). + var defaults = await _db.DefaultDecks.OrderBy(d => d.Id).ToListAsync(); + resp.DefaultDeckList = defaults.ToDictionary( + d => d.Id.ToString(), + d => new DefaultDeck + { + DeckNo = d.DeckNo, + ClassId = d.ClassId, + SleeveId = d.SleeveId, + LeaderSkinId = d.LeaderSkinId, + DeckName = d.DeckName, + CardIdArray = JsonSerializer.Deserialize>(d.CardIdArray) ?? new(), + IsCompleteDeck = 1, + IsAvailableDeck = 1, + MaintenanceCardIds = new(), + }); + + return resp; } + + private static BuildDeck ToBuildDeck(StoryDeckView d) => new() + { + DeckNo = d.DeckNo, + OrderNum = d.OrderNum, + ClassId = d.ClassId, + SleeveId = d.SleeveId, + LeaderSkinId = d.LeaderSkinId, + EntryNo = d.EntryNo, + CreateDeckTime = null, + DeckName = d.DeckName, + CardIdArray = d.CardIdArray, + IsCompleteDeck = 1, + IsAvailableDeck = 1, + MaintenanceCardIds = new(), + IsRecommend = d.IsRecommend, + }; + + private static TrialDeck ToTrialDeck(StoryDeckView d) => new() + { + DeckNo = d.DeckNo, + ClassId = d.ClassId, + SleeveId = d.SleeveId, + LeaderSkinId = d.LeaderSkinId, + DeckName = d.DeckName, + CardIdArray = d.CardIdArray, + IsCompleteDeck = 1, + RestrictedCardExists = false, + IsAvailableDeck = 1, + MaintenanceCardIds = new(), + IsIncludeUnPossessionCard = false, + DeckFormat = d.DeckFormat ?? 0, + IsRecommend = d.IsRecommend, + }; public async Task StartAsync(StoryApiType apiType, int[] storyIds, long viewerId) { var resp = new StartResponse(); diff --git a/SVSim.UnitTests/Story/StoryDeckListServiceTests.cs b/SVSim.UnitTests/Story/StoryDeckListServiceTests.cs new file mode 100644 index 0000000..09482b0 --- /dev/null +++ b/SVSim.UnitTests/Story/StoryDeckListServiceTests.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using SVSim.Database; +using SVSim.Database.Entities.Story; +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.EmulatedEntrypoint.Services; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Story; + +[TestFixture] +public class StoryDeckListServiceTests +{ + [Test] + public async Task GetDeckList_populates_build_trial_and_default_for_chapter_class() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // StoryChapter.SectionId has an enforced FK to StorySection; seed the parent row first. + db.StorySections.Add(new StorySection { Id = 1, StoryApiType = StoryApiType.Main }); + // Chapter 14 is a class-1 (Forestcraft) chapter. + db.StoryChapters.Add(new StoryChapter { StoryId = 14, SectionId = 1, CharaId = 1, ChapterId = "14" }); + + // One class-1 build deck (701) + one class-1 trial deck (13001), each with a 1-card product. + // BuildDeckProductEntry has an enforced FK SeriesId -> BuildDeckSeries; seed the parent first. + db.BuildDeckSeries.Add(new BuildDeckSeriesEntry { Id = 0 }); + db.BuildDeckProducts.Add(new BuildDeckProductEntry { Id = 701, Cards = new() { new BuildDeckProductCardEntry { CardId = 100, Number = 1 } } }); + db.BuildDeckProducts.Add(new BuildDeckProductEntry { Id = 13001, Cards = new() { new BuildDeckProductCardEntry { CardId = 200, Number = 1 } } }); + db.StoryDecks.Add(new StoryDeckEntry { DeckNo = 701, Kind = StoryDeckKind.Build, ClassId = 1, DeckName = "Pure Devotion", DeckFormat = null }); + db.StoryDecks.Add(new StoryDeckEntry { DeckNo = 13001, Kind = StoryDeckKind.Trial, ClassId = 1, DeckName = "Tempo Forestcraft", DeckFormat = 1 }); + + db.DefaultDecks.Add(new DefaultDeckEntry { Id = 91, ClassId = 1, SleeveId = 3000011, LeaderSkinId = 0, DeckName = "Default", CardIdArray = "[100,100,100]" }); + await db.SaveChangesAsync(); + + var service = scope.ServiceProvider.GetRequiredService(); + var resp = await service.GetDeckListAsync(StoryApiType.Main, storyId: 14, viewerId: 1); + + Assert.That(resp.BuildDeckList.Count, Is.EqualTo(1)); + Assert.That(resp.BuildDeckList[0].DeckNo, Is.EqualTo(701)); + Assert.That(resp.BuildDeckList[0].DeckName, Is.EqualTo("Pure Devotion")); + Assert.That(resp.BuildDeckList[0].CardIdArray, Is.EqualTo(new long[] { 100 })); + + Assert.That(resp.TrialDeckList.Count, Is.EqualTo(1)); + Assert.That(resp.TrialDeckList[0].DeckNo, Is.EqualTo(13001)); + Assert.That(resp.TrialDeckList[0].DeckFormat, Is.EqualTo(1)); + + Assert.That(resp.DefaultDeckList.ContainsKey("91"), Is.True); + Assert.That(resp.DefaultDeckList["91"].CardIdArray, Is.EqualTo(new long[] { 100, 100, 100 })); + } + + [Test] + public async Task GetDeckList_returns_empty_build_trial_for_non_class_chapter() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // StoryChapter.SectionId has an enforced FK to StorySection; seed the parent row first. + db.StorySections.Add(new StorySection { Id = 17, StoryApiType = StoryApiType.Main }); + // chara_id 0 -> custom-leader / non-class chapter. + db.StoryChapters.Add(new StoryChapter { StoryId = 500, SectionId = 17, CharaId = 0, ChapterId = "500" }); + await db.SaveChangesAsync(); + + var service = scope.ServiceProvider.GetRequiredService(); + var resp = await service.GetDeckListAsync(StoryApiType.Main, storyId: 500, viewerId: 1); + + Assert.That(resp.BuildDeckList, Is.Empty); + Assert.That(resp.TrialDeckList, Is.Empty); + } +} diff --git a/SVSim.UnitTests/Story/StoryServiceTests.cs b/SVSim.UnitTests/Story/StoryServiceTests.cs index 4bdefc9..1c80aa5 100644 --- a/SVSim.UnitTests/Story/StoryServiceTests.cs +++ b/SVSim.UnitTests/Story/StoryServiceTests.cs @@ -35,6 +35,7 @@ public class StoryServiceTests db: db, configService: StoryServiceTestHelpers.NewConfigService(), deckRepository: new Mock().Object, + buildDecks: new Mock().Object, logger: NullLogger.Instance); } @@ -72,6 +73,7 @@ public class StoryServiceTests db: db, configService: StoryServiceTestHelpers.NewConfigService(), deckRepository: new Mock().Object, + buildDecks: new Mock().Object, logger: NullLogger.Instance); } @@ -404,6 +406,7 @@ public class StoryServiceTests db: db, configService: StoryServiceTestHelpers.NewConfigService(), deckRepository: new Mock().Object, + buildDecks: new Mock().Object, logger: NullLogger.Instance); }