Sibling to BuildForTwoPickAsync. Routes through IDeckRepository.GetDeck to pull the viewer's deck #1 for the requested format (avoiding the viewer-graph nav-ref auto-load pitfall — DeckCard.Card silently ships card_id=0 via the default include path). Throws if the viewer has no deck for the format. Cosmetics fall back to DefaultLoadoutConfig defaults when unequipped, same shape as TK2. Used by RankBattleController in a later task to build self-context for /ai_<fmt>_rank_battle/start and to pair-up under /<fmt>_rank_battle/do_matching. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
115 lines
4.7 KiB
C#
115 lines
4.7 KiB
C#
using System.Text.Json;
|
|
using SVSim.BattleNode.Bridge;
|
|
using SVSim.Database.Enums;
|
|
using SVSim.Database.Models.Config;
|
|
using SVSim.Database.Repositories.Deck;
|
|
using SVSim.Database.Repositories.Viewer;
|
|
using SVSim.Database.Services;
|
|
|
|
namespace SVSim.EmulatedEntrypoint.Services;
|
|
|
|
public class MatchContextBuilder : IMatchContextBuilder
|
|
{
|
|
private readonly IArenaTwoPickRunRepository _runs;
|
|
private readonly IViewerRepository _viewers;
|
|
private readonly IDeckRepository _decks;
|
|
private readonly IGameConfigService _config;
|
|
|
|
public MatchContextBuilder(
|
|
IArenaTwoPickRunRepository runs,
|
|
IViewerRepository viewers,
|
|
IDeckRepository decks,
|
|
IGameConfigService config)
|
|
{
|
|
_runs = runs;
|
|
_viewers = viewers;
|
|
_decks = decks;
|
|
_config = config;
|
|
}
|
|
|
|
public async Task<MatchContext> BuildForTwoPickAsync(long viewerId)
|
|
{
|
|
var run = await _runs.GetByViewerIdAsync(viewerId)
|
|
?? throw new ArenaTwoPickException("arena_two_pick_no_active_run");
|
|
|
|
var deck = JsonSerializer.Deserialize<List<long>>(run.SelectedCardIdsJson) ?? new();
|
|
if (deck.Count < 30)
|
|
throw new ArenaTwoPickException("arena_two_pick_draft_incomplete");
|
|
|
|
var viewer = await _viewers.LoadForMatchContextAsync(viewerId)
|
|
?? throw new ArenaTwoPickException("arena_two_pick_no_active_run");
|
|
|
|
var challenge = _config.Get<ChallengeConfig>();
|
|
var defaults = _config.Get<DefaultLoadoutConfig>();
|
|
|
|
var emblemId = viewer.Info.SelectedEmblem.Id != 0
|
|
? viewer.Info.SelectedEmblem.Id.ToString()
|
|
: defaults.EmblemId.ToString();
|
|
var degreeId = viewer.Info.SelectedDegree.Id != 0
|
|
? viewer.Info.SelectedDegree.Id.ToString()
|
|
: defaults.DegreeId.ToString();
|
|
var charaId = run.LeaderSkinId != 0
|
|
? run.LeaderSkinId.ToString()
|
|
: run.ClassId.ToString();
|
|
|
|
return new MatchContext(
|
|
SelfDeckCardIds: deck,
|
|
ClassId: run.ClassId.ToString(),
|
|
CharaId: charaId,
|
|
// Hardcoded v1; see spec §Deferred plumbing.
|
|
CardMasterName: "card_master_node_10015",
|
|
CountryCode: viewer.Info.CountryCode ?? string.Empty,
|
|
UserName: viewer.DisplayName,
|
|
// TK2-specific cosmetic source; other modes will use the deck row's SleeveId.
|
|
SleeveId: challenge.TwoPickSleeveId.ToString(),
|
|
EmblemId: emblemId,
|
|
DegreeId: degreeId,
|
|
// Hardcoded v1; needs equipped-MyPageBackground lookup (see spec §Deferred).
|
|
FieldId: 43,
|
|
IsOfficial: viewer.Info.IsOfficial ? 1 : 0,
|
|
BattleType: 11);
|
|
}
|
|
|
|
public async Task<MatchContext> BuildForRankBattleAsync(long viewerId, Format format)
|
|
{
|
|
var viewer = await _viewers.LoadForMatchContextAsync(viewerId)
|
|
?? throw new InvalidOperationException($"viewer {viewerId} not found");
|
|
|
|
// Per spec, deck-selection persistence (which deck number is "current" for this
|
|
// format) is a separate concern; #1 is a placeholder until that lands. IDeckRepository
|
|
// is the right path here — viewer-graph nav refs (DeckCard.Card) don't auto-load
|
|
// (see project_ef_nav_include_pitfall memory), which would silently ship card_id=0.
|
|
var deck = await _decks.GetDeck(viewerId, format, deckNo: 1)
|
|
?? throw new InvalidOperationException($"viewer {viewerId} has no deck for format {format}");
|
|
|
|
var defaults = _config.Get<DefaultLoadoutConfig>();
|
|
var emblemId = viewer.Info.SelectedEmblem.Id != 0
|
|
? viewer.Info.SelectedEmblem.Id.ToString()
|
|
: defaults.EmblemId.ToString();
|
|
var degreeId = viewer.Info.SelectedDegree.Id != 0
|
|
? viewer.Info.SelectedDegree.Id.ToString()
|
|
: defaults.DegreeId.ToString();
|
|
var charaId = deck.LeaderSkin.Id != 0
|
|
? deck.LeaderSkin.Id.ToString()
|
|
: deck.Class.Id.ToString();
|
|
var sleeveId = deck.Sleeve.Id != 0
|
|
? deck.Sleeve.Id.ToString()
|
|
: defaults.SleeveId.ToString();
|
|
var deckCardIds = deck.Cards.Select(c => c.Card.Id).ToList();
|
|
|
|
return new MatchContext(
|
|
SelfDeckCardIds: deckCardIds,
|
|
ClassId: deck.Class.Id.ToString(),
|
|
CharaId: charaId,
|
|
CardMasterName: "card_master_node_10015",
|
|
CountryCode: viewer.Info.CountryCode ?? string.Empty,
|
|
UserName: viewer.DisplayName,
|
|
SleeveId: sleeveId,
|
|
EmblemId: emblemId,
|
|
DegreeId: degreeId,
|
|
FieldId: 43,
|
|
IsOfficial: viewer.Info.IsOfficial ? 1 : 0,
|
|
BattleType: 11);
|
|
}
|
|
}
|