feat(story): populate build/trial/default deck lists on get_deck_list
Wire IBuildDeckRepository into StoryService; GetDeckListAsync now looks up the chapter's CharaId, fetches class-specific prebuilt/trial decks via GetStoryDecksByClass, and loads all DefaultDecks for default_deck_list. Class guard (1-8) leaves build/trial empty for non-class chapters, matching prod behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,16 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Text.Json;
|
||||||
using SVSim.Database;
|
using SVSim.Database;
|
||||||
using SVSim.Database.Entities.Story;
|
using SVSim.Database.Entities.Story;
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
using SVSim.Database.Models.Config;
|
using SVSim.Database.Models.Config;
|
||||||
using SVSim.Database.Repositories.Deck;
|
using SVSim.Database.Repositories.Deck;
|
||||||
|
using SVSim.Database.Repositories.BuildDeck;
|
||||||
using SVSim.Database.Services;
|
using SVSim.Database.Services;
|
||||||
using SVSim.Database.Repositories.Story;
|
using SVSim.Database.Repositories.Story;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Common;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Story;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Story;
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Services;
|
namespace SVSim.EmulatedEntrypoint.Services;
|
||||||
@@ -19,6 +23,7 @@ public class StoryService : IStoryService
|
|||||||
private readonly SVSimDbContext _db;
|
private readonly SVSimDbContext _db;
|
||||||
private readonly IGameConfigService _configService;
|
private readonly IGameConfigService _configService;
|
||||||
private readonly IDeckRepository _deckRepository;
|
private readonly IDeckRepository _deckRepository;
|
||||||
|
private readonly IBuildDeckRepository _buildDecks;
|
||||||
private readonly ILogger<StoryService> _logger;
|
private readonly ILogger<StoryService> _logger;
|
||||||
|
|
||||||
public StoryService(
|
public StoryService(
|
||||||
@@ -28,6 +33,7 @@ public class StoryService : IStoryService
|
|||||||
SVSimDbContext db,
|
SVSimDbContext db,
|
||||||
IGameConfigService configService,
|
IGameConfigService configService,
|
||||||
IDeckRepository deckRepository,
|
IDeckRepository deckRepository,
|
||||||
|
IBuildDeckRepository buildDecks,
|
||||||
ILogger<StoryService> logger)
|
ILogger<StoryService> logger)
|
||||||
{
|
{
|
||||||
_master = master;
|
_master = master;
|
||||||
@@ -36,6 +42,7 @@ public class StoryService : IStoryService
|
|||||||
_db = db;
|
_db = db;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_deckRepository = deckRepository;
|
_deckRepository = deckRepository;
|
||||||
|
_buildDecks = buildDecks;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,16 +351,85 @@ public class StoryService : IStoryService
|
|||||||
{
|
{
|
||||||
var byFormat = await _deckRepository.GetDecksByFormats(
|
var byFormat = await _deckRepository.GetDecksByFormats(
|
||||||
viewerId, new[] { SVSim.Database.Enums.Format.Rotation, SVSim.Database.Enums.Format.Unlimited });
|
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]
|
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]
|
UserDeckUnlimited = byFormat[SVSim.Database.Enums.Format.Unlimited]
|
||||||
.Select(d => new SVSim.EmulatedEntrypoint.Models.Dtos.UserDeck(d)).ToList(),
|
.Select(d => new UserDeck(d)).ToList(),
|
||||||
BuildDeckList = new List<BuildDeck>(), // v1: empty
|
|
||||||
MaintenanceCardList = new List<long>(),
|
MaintenanceCardList = new List<long>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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<List<long>>(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<StartResponse> StartAsync(StoryApiType apiType, int[] storyIds, long viewerId)
|
public async Task<StartResponse> StartAsync(StoryApiType apiType, int[] storyIds, long viewerId)
|
||||||
{
|
{
|
||||||
var resp = new StartResponse();
|
var resp = new StartResponse();
|
||||||
|
|||||||
74
SVSim.UnitTests/Story/StoryDeckListServiceTests.cs
Normal file
74
SVSim.UnitTests/Story/StoryDeckListServiceTests.cs
Normal file
@@ -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<SVSimDbContext>();
|
||||||
|
|
||||||
|
// 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<IStoryService>();
|
||||||
|
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<SVSimDbContext>();
|
||||||
|
|
||||||
|
// 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<IStoryService>();
|
||||||
|
var resp = await service.GetDeckListAsync(StoryApiType.Main, storyId: 500, viewerId: 1);
|
||||||
|
|
||||||
|
Assert.That(resp.BuildDeckList, Is.Empty);
|
||||||
|
Assert.That(resp.TrialDeckList, Is.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ public class StoryServiceTests
|
|||||||
db: db,
|
db: db,
|
||||||
configService: StoryServiceTestHelpers.NewConfigService(),
|
configService: StoryServiceTestHelpers.NewConfigService(),
|
||||||
deckRepository: new Mock<SVSim.Database.Repositories.Deck.IDeckRepository>().Object,
|
deckRepository: new Mock<SVSim.Database.Repositories.Deck.IDeckRepository>().Object,
|
||||||
|
buildDecks: new Mock<SVSim.Database.Repositories.BuildDeck.IBuildDeckRepository>().Object,
|
||||||
logger: NullLogger<StoryService>.Instance);
|
logger: NullLogger<StoryService>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ public class StoryServiceTests
|
|||||||
db: db,
|
db: db,
|
||||||
configService: StoryServiceTestHelpers.NewConfigService(),
|
configService: StoryServiceTestHelpers.NewConfigService(),
|
||||||
deckRepository: new Mock<SVSim.Database.Repositories.Deck.IDeckRepository>().Object,
|
deckRepository: new Mock<SVSim.Database.Repositories.Deck.IDeckRepository>().Object,
|
||||||
|
buildDecks: new Mock<SVSim.Database.Repositories.BuildDeck.IBuildDeckRepository>().Object,
|
||||||
logger: NullLogger<StoryService>.Instance);
|
logger: NullLogger<StoryService>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +406,7 @@ public class StoryServiceTests
|
|||||||
db: db,
|
db: db,
|
||||||
configService: StoryServiceTestHelpers.NewConfigService(),
|
configService: StoryServiceTestHelpers.NewConfigService(),
|
||||||
deckRepository: new Mock<SVSim.Database.Repositories.Deck.IDeckRepository>().Object,
|
deckRepository: new Mock<SVSim.Database.Repositories.Deck.IDeckRepository>().Object,
|
||||||
|
buildDecks: new Mock<SVSim.Database.Repositories.BuildDeck.IBuildDeckRepository>().Object,
|
||||||
logger: NullLogger<StoryService>.Instance);
|
logger: NullLogger<StoryService>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user