fix(tk2): honor consume_item_type (ticket/crystal/rupy/free) + correct entry ticket id
- ArenaTwoPickConfig: add TicketItemId=1, TicketCost=1, CrystalCost=150, RupyCost=150 scalars - ArenaTwoPickService.EntryAsync: switch on eARENA_PAY (1/3/4/5); crystal/rupy go through ICurrencySpendService.TrySpendAsync; ticket uses item id 1 (challenge ticket, not 80001); free entry returns empty reward_list; invalid type throws - Tests: fix ticket id 80001→1 in entry/e2e; add 4 new path tests; update ctor (10th arg) across all 4 service test files; fix e2e retire assertion (reward ticket 80001 post-state=1) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
||||
private readonly IViewerEntitlements _entitlements;
|
||||
private readonly IRandom _rng;
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly ICurrencySpendService _spend;
|
||||
|
||||
public ArenaTwoPickService(
|
||||
IArenaTwoPickRunRepository runs,
|
||||
@@ -31,10 +32,12 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
||||
RewardGrantService grants,
|
||||
IViewerEntitlements entitlements,
|
||||
IRandom rng,
|
||||
SVSimDbContext db)
|
||||
SVSimDbContext db,
|
||||
ICurrencySpendService spend)
|
||||
{
|
||||
_runs = runs; _rewards = rewards; _pool = pool; _config = config;
|
||||
_viewers = viewers; _grants = grants; _entitlements = entitlements; _rng = rng; _db = db;
|
||||
_spend = spend;
|
||||
}
|
||||
|
||||
public async Task<TopResponseDto> GetTopAsync(long viewerId)
|
||||
@@ -62,37 +65,20 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
||||
if (await _runs.GetByViewerIdAsync(viewerId) is not null)
|
||||
throw new ArenaTwoPickException("arena_two_pick_already_in_progress");
|
||||
|
||||
const long ticketItemId = 80001;
|
||||
|
||||
var viewer = await LoadViewerForGrantsAsync(viewerId);
|
||||
var ticket = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)ticketItemId);
|
||||
int postStateTickets;
|
||||
if (_entitlements.IsFreeplay)
|
||||
{
|
||||
postStateTickets = ticket?.Count ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ticket is null || ticket.Count < 1)
|
||||
throw new ArenaTwoPickException("insufficient_ticket");
|
||||
ticket.Count -= 1;
|
||||
postStateTickets = ticket.Count;
|
||||
}
|
||||
|
||||
var aCfg = _config.Get<SVSim.Database.Models.Config.ArenaTwoPickConfig>();
|
||||
var rawMaxWins = await _rewards.GetMaxWinCountAsync();
|
||||
int maxWins;
|
||||
if (rawMaxWins == 0)
|
||||
var viewer = await LoadViewerForGrantsAsync(viewerId);
|
||||
|
||||
// Dispatch on the client's chosen payment method (ArenaData.eARENA_PAY).
|
||||
RewardEntryDto? feeEntry = consumeItemType switch
|
||||
{
|
||||
// Reward catalog not seeded — bootstrap hasn't run. Fall back to the spec's
|
||||
// documented default (7) and log a warning so misconfigured deployments are visible.
|
||||
Console.Error.WriteLine("[ArenaTwoPickService] ArenaTwoPickRewards catalog empty; defaulting MaxBattleCount=7. Run SVSim.Bootstrap to seed.");
|
||||
maxWins = 7;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxWins = rawMaxWins;
|
||||
}
|
||||
1 => await DebitCrystalsAsync(viewer, aCfg.CrystalCost),
|
||||
3 => DebitTicket(viewer, aCfg.TicketItemId, aCfg.TicketCost),
|
||||
4 => await DebitRupiesAsync(viewer, aCfg.RupyCost),
|
||||
5 => null, // Free entry — no fee.
|
||||
_ => throw new ArenaTwoPickException("invalid_consume_item_type"),
|
||||
};
|
||||
|
||||
var maxWins = await ResolveMaxBattleCountAsync();
|
||||
var candidates = SampleCandidateClasses(aCfg.AllowedClassIds, _rng);
|
||||
|
||||
var run = new ViewerArenaTwoPickRun
|
||||
@@ -120,18 +106,77 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
||||
await _runs.UpsertAsync(run);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var rewardList = feeEntry is null ? new List<RewardEntryDto>() : new List<RewardEntryDto> { feeEntry };
|
||||
|
||||
return new EntryResponseDto
|
||||
{
|
||||
EntryInfo = ProjectEntryInfo(run, viewerId),
|
||||
RewardList = new List<RewardEntryDto>
|
||||
{
|
||||
new RewardEntryDto { RewardType = 4, RewardId = ticketItemId, RewardNum = postStateTickets },
|
||||
},
|
||||
RewardList = rewardList,
|
||||
CandidateClassIds = candidates,
|
||||
BattleResults = new BattleResultsDto { WinCount = 0, ResultList = new List<int>() },
|
||||
};
|
||||
}
|
||||
|
||||
private RewardEntryDto DebitTicket(SVSim.Database.Models.Viewer viewer, int ticketItemId, int ticketCost)
|
||||
{
|
||||
var ticket = viewer.Items.FirstOrDefault(i => i.Item.Id == ticketItemId);
|
||||
int postStateCount;
|
||||
if (_entitlements.IsFreeplay)
|
||||
{
|
||||
postStateCount = ticket?.Count ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ticket is null || ticket.Count < ticketCost)
|
||||
throw new ArenaTwoPickException("insufficient_ticket");
|
||||
ticket.Count -= ticketCost;
|
||||
postStateCount = ticket.Count;
|
||||
}
|
||||
return new RewardEntryDto
|
||||
{
|
||||
RewardType = (int)SVSim.Database.Enums.UserGoodsType.Item,
|
||||
RewardId = ticketItemId,
|
||||
RewardNum = postStateCount,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<RewardEntryDto> DebitCrystalsAsync(SVSim.Database.Models.Viewer viewer, int cost)
|
||||
{
|
||||
var result = await _spend.TrySpendAsync(viewer, SVSim.Database.Services.SpendCurrency.Crystal, cost);
|
||||
if (!result.Success)
|
||||
throw new ArenaTwoPickException("insufficient_crystal");
|
||||
return new RewardEntryDto
|
||||
{
|
||||
RewardType = (int)SVSim.Database.Enums.UserGoodsType.Crystal,
|
||||
RewardId = 0,
|
||||
RewardNum = (int)result.PostStateTotal,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<RewardEntryDto> DebitRupiesAsync(SVSim.Database.Models.Viewer viewer, int cost)
|
||||
{
|
||||
var result = await _spend.TrySpendAsync(viewer, SVSim.Database.Services.SpendCurrency.Rupee, cost);
|
||||
if (!result.Success)
|
||||
throw new ArenaTwoPickException("insufficient_rupy");
|
||||
return new RewardEntryDto
|
||||
{
|
||||
RewardType = (int)SVSim.Database.Enums.UserGoodsType.Rupy,
|
||||
RewardId = 0,
|
||||
RewardNum = (int)result.PostStateTotal,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<int> ResolveMaxBattleCountAsync()
|
||||
{
|
||||
var rawMaxWins = await _rewards.GetMaxWinCountAsync();
|
||||
if (rawMaxWins == 0)
|
||||
{
|
||||
Console.Error.WriteLine("[ArenaTwoPickService] ArenaTwoPickRewards catalog empty; defaulting MaxBattleCount=7. Run SVSim.Bootstrap to seed.");
|
||||
return 7;
|
||||
}
|
||||
return rawMaxWins;
|
||||
}
|
||||
|
||||
private static List<int> SampleCandidateClasses(List<int> allowed, IRandom rng)
|
||||
{
|
||||
if (allowed.Count < 3)
|
||||
|
||||
Reference in New Issue
Block a user