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:
gamer147
2026-05-31 12:26:57 -04:00
parent 668779e8a4
commit dc19289818
7 changed files with 166 additions and 51 deletions

View File

@@ -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)