79 Commits

Author SHA1 Message Date
gamer147
80f249f8a2 feat(ranking): add RankingPeriodSchedule helper
Pure deterministic monthly period generator for the four ranking
families. Anchor dates derived from prod capture (2026-06-09): id=1 is
each family's launch month in JST; id=N is anchor + N-1 months. Used
by /ranking/get_viewable_ranking_period_list to render the period
picker and by per-family leaderboard endpoints to echo the requested
period back.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 10:36:51 -04:00
gamer147
2b6c7bd6a4 feat(replay): add BattleHistoryWriter with 50-row per-viewer retention
Idempotent on (ViewerId, BattleId); evicts oldest CreateTime row when
at cap. No-op when ctx is null (server-restart safety).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 07:43:50 -04:00
gamer147
869f9ce13d feat(replay): add BattleContextStore for start->finish handoff
Bridges the start-time -> finish-time gap. /finish carries neither
battle_id nor opponent identity; this store holds both for the finish
handler to compose a ViewerBattleHistory row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 07:38:37 -04:00
gamer147
b5b4781693 feat(friend): bulk apply ops + IPlayedTogetherWriter with retention cap
Implements RejectAllAppliesAsync, CancelAllAppliesAsync (ExecuteDelete bulk
deletes on incoming/outgoing applies respectively) and RecordAsync (upsert
played-together row with 50-row per-viewer retention eviction). 4 new tests
added; all 1186 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:04:04 -04:00
gamer147
17591a6ebd feat(friend): implement single-target write methods on FriendService
SendApplyAsync, ApproveApplyAsync, RejectApplyAsync, CancelApplyAsync,
RejectFriendAsync all implemented. 11 new tests appended; all 23 friend
tests pass, full suite 1182/1182 green.
2026-06-09 21:59:32 -04:00
gamer147
d078f275f8 feat(friend): implement 5 read methods on FriendService + register DI + read test suite
GetFriendsAsync, GetReceiveAppliesAsync, GetSendAppliesAsync, GetPlayedTogetherAsync,
SearchAsync all implemented. LoadViewerProjectionAsync materialises the full Viewer
entity (with Include/ThenInclude for SelectedEmblem/Degree) then projects in-memory —
avoids the EF Core limitation where Include is silently ignored under Select.
FriendService + IPlayedTogetherWriter registered as Scoped in Program.cs.
12 read tests, all green; full suite 1171/1171 still passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:53:10 -04:00
gamer147
366a71688d feat(gift): add GiftRewardTypes supported-set helper
Centralizes which UserGoodsType values IInventoryTransaction.GrantAsync
can handle in the gift-inbox flow; both GiftController (post-fix) and
CampaignController will consult this instead of duplicating the set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:39:10 -04:00
gamer147
8de78ba7ed test(inventory): lock Unknown-source fallthrough behaviour
Adds Commit_writes_history_row_with_Unknown_source_when_caller_omits_Source
to InventoryHistoryTests — asserts that BeginAsync without a configure callback
writes AcquireType=0 / Message="Unknown", per spec §10.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:33:04 -04:00
gamer147
9130d6de11 test(inventory): pack-open shape produces acquire history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:59:39 -04:00
gamer147
77ad614258 feat(inventory): prune acquire history above 300-row cap
Adds PruneAcquireHistoryAsync to InventoryTransaction.CommitAsync; runs
inside the open DB transaction after history rows are flushed, keeping at
most 300 rows per viewer (oldest discarded). Adds a covering test that
verifies the cap and per-viewer isolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:41:04 -04:00
gamer147
fb1e6829b7 refactor(inventory): consolidate IsCurrency, skip num=0 grants in history
- Drop IsWalletCurrency (duplicate of IsCurrency); use IsCurrency in WriteAcquireHistory.
- Add comment on first SaveChangesAsync in CommitAsync explaining the two-phase flush.
- Guard WriteAcquireHistory loop with grant.Num == 0 check so synthetic DebitItem post-state ops do not produce history rows.
- Add InventoryHistoryTests.Commit_writes_no_history_row_for_item_debit to lock in the fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:37:50 -04:00
gamer147
bea5a1efd4 feat(inventory): write acquire history rows on commit
CommitAsync now calls WriteAcquireHistory() between the two SaveChanges
calls: iterates _ops, skips SpendOps, writes one ViewerAcquireHistoryEntry
per GrantOp. Cascade rows get GrantSource.CardCosmeticCascade; wallet
currencies zero RewardDetailId; all rows in a single commit share one
DateTime.UtcNow timestamp. Closes _source plumbing from Task 5.

5 new tests added (46 total inventory, 0 failures).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:32:51 -04:00
gamer147
0d036e1bff feat(inventory): add ViewerAcquireHistoryEntry entity + DbSet
Adds the ViewerAcquireHistoryEntry model (8 fields: Id, ViewerId,
RewardType, RewardDetailId, RewardCount, AcquireType, Message,
AcquireTime), registers DbSet<ViewerAcquireHistoryEntry> on
SVSimDbContext, configures model (PK, FK cascade to Viewer, MaxLength
64 on Message, composite index on ViewerId/AcquireTime/Id), and adds a
DbSet round-trip integration test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:19:00 -04:00
gamer147
82dc877639 feat(inventory): add Source to InventoryLoadConfig
Adds a `GrantSource Source { get; set; }` property (defaults to
`GrantSource.Unknown`) to `InventoryLoadConfig`. Plumbing-only — no
behavior change; callers that don't set `Source` get Unknown rows,
greppable via `acquire_type=0` in dev.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:13:56 -04:00
gamer147
9f65326449 feat(inventory): add GrantSource enum + message lookup
Introduces GrantSource (17 values, DB-persisted) and GrantSourceMessages.For()
for the item-acquire-history feature. Values 1/2 mirror prod captures;
coverage test verifies every enum value has a non-empty message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:08:01 -04:00
gamer147
7118b92522 refactor(pack): type PackChildGachaEntry.TypeDetail as CardPackType enum 2026-06-09 08:48:16 -04:00
gamer147
7e757ebcd2 feat(home-dialog): per-session suppression tracker
Singleton keyed by ShortUdid; lock on per-viewer set to avoid
cross-viewer contention. Process lifetime — restart re-fires.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:53:33 -04:00
gamer147
1007cf24d2 refactor(battlenode): type MatchContext.ClassId as CardClass enum (§C)
Behavior-preserving; full solution builds, 1013 tests green.

ClassId is the one genuinely-closed set of the three flagged stringly fields, so it
becomes a CardClass enum (1..8). Wire stays "1".."8": producer casts
(CardClass)run.ClassId, ServerBattleFrames renders via CardClassWire.ToWireValue().
RankBattleController's AI-start path drops a fragile int.TryParse(...)?:-1 for (int)cast.

CharaId (free-form leader/skin id, e.g. "5000123") and CountryCode (open-ended account
data) stay string with proper XML docs; CountryCodes.Korea/Japan name the captured values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:04:49 -04:00
gamer147
578d0a75ef refactor(battlenode): rename mode-id field off BattleType, add BattleModes (§D)
Behavior-preserving; 271 BattleNode/Matching/Services tests green, full solution builds.

"BattleType" meant two things: the Sessions.BattleType enum (Pvp/Bot) and an int
"mode id" field. Renamed the int field on MatchContext AND the BattleStartBody wire
DTO to BattleModeId (wire key stays "battleType" via JsonPropertyName), so BattleType
now means only the enum project-wide.

New Bridge/BattleModes.cs (TakeTwo = 11) replaces every 11 literal — both prod
MatchContextBuilder sites and the test fixtures/assertions. The arbitrary-passthrough
42 and bot 0 stay literal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:44:02 -04:00
gamer147
56652c7034 fix(battle-node): expand rank-battle deck by DeckCard.Count
BuildForRankBattleAsync projected deck.Cards.Select(c => c.Card.Id),
discarding Count. DeckCard is count-based (one row per unique card +
a Count), so a 3-copy card shipped to the node as a single in-battle
card -- matched decks showed 1 of each card instead of the real count.

Expand each row by its Count so SelfDeckCardIds carries one entry per
physical card. TwoPick path is unaffected (flat per-pick list).

Add a regression test seeding 3+2+1 copies (failed Expected 6/was 3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:09:14 -04:00
gamer147
c27bf444a5 refactor(auth): drop null-guard on dev steam ticket log; add test fixture doc
ISteamServer contract forbids null tickets (prod impl and sole caller both assume non-null),
so the dev bypass no longer needs the ?. / ?? 0 defensive form. Also adds a class-level XML
doc summary to DevAlwaysValidSteamServerTests matching the style of other fixtures in the suite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 09:06:26 -04:00
gamer147
ae94d62357 feat(auth): add Dev-only always-valid ISteamServer for local no-Steam clients
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 09:02:54 -04:00
gamer147
05d8169012 refactor: type reward_type columns as UserGoodsType enum
Replace bare `int RewardType` on 12 catalog/reward entities and GrantedReward
with the existing UserGoodsType enum. Verified against the decompiled client:
every wire reward_type decodes through the single Wizard.UserGoods.Type enum, so
one enum is correct across all endpoint families (item_type is a separate
Item.Type axis, left untouched). EF stores the enum as the same int column, so
there is no migration.

- Importers cast seed int -> UserGoodsType at the ingest boundary.
- New GrantedReward.ToRewardList() extension replaces 8 copy-pasted
  GrantedReward -> RewardListEntry projections.
- Fix 3 .ToString() sites that would otherwise emit enum names ("Crystal")
  instead of the int wire value ("2").
- Wire DTOs keep int; the enum is widened to int at the wire boundary only.

Build green; 962/962 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:50:49 -04:00
gamer147
898b872edd fix(rank-battle): route ai-start through the queue-time MatchContext
Live-smoke bug 2026-06-02: queued Bloodcraft (deck #5), wire showed
classId=2 (Swordcraft) for self_info on the /ai_unlimited_rank_battle/start
response — client rendered the wrong leader.

Two layers of the same bug:

1. MatchContextBuilder.BuildForRankBattleAsync hardcoded deckNo=1 instead
   of taking it from the do_matching request — verified against
   data_dumps/captures/traffic.ndjson L17 where deck_no=5 was on the wire.
   Signature changes to (viewerId, format, deckNo); DoMatchingInternal
   passes req.DeckNo.

2. AiStartInternal rebuilt MatchContext from scratch — but the /ai_*/start
   request body is BaseRequest only, no deck_no on the wire. The fix uses
   the MatchContext the bridge already stored at do_matching resolution time
   (in the Bot PendingBattle), so deck/cosmetic data is consistent end-to-end.
   New IBattleSessionStore.TryFindPendingForViewer(viewerId) finds the
   viewer's pending battle for lookup. The store entry persists across
   ai_start (idempotent reads are fine — the WS handler removes on connect).
   No-pending sentinel: ai_id=-1 surfaces the "no AI assigned" error in the
   client.

Tests: 936 → 939 passing.
- MatchContextBuilderTests.BuildForRankBattle_uses_the_caller_supplied_deck_number
  seeds deck #1 (class 1) and deck #5 (class 6) and asserts the deckNo
  argument picks the right one.
- RankBattleControllerTests.AiStart_self_info_class_matches_queued_deck_number
  is the end-to-end regression: register Bot battle with deck #5, hit
  /ai_unlimited_rank_battle/start, assert self_info.classId == 6.
- RankBattleControllerTests.AiStart_without_pending_battle_returns_neg1_sentinel
  locks the defensive ai_id=-1 path.
- Existing AiStart_* tests bypass do_matching, so adapted to call a new
  RegisterBotBattleAsync helper that mirrors what InProcessPairUp does on
  AI-fallback resolution.

SeedDeckAsync gains an optional classId so test cases can differentiate
decks by class (was always picking Classes.First()).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 12:28:42 -04:00
gamer147
7eaf13893e feat(matching): MatchContextBuilder.BuildForRankBattleAsync for rank battles
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>
2026-06-02 01:13:19 -04:00
gamer147
a0fdb0f3c5 feat(match-context): add IMatchContextBuilder TK2 implementation
Assembles MatchContext from ArenaTwoPickRun + viewer cosmetics + config.
Per-mode interface — future modes (rank/free/open-room/...) add one method
each. DI scoped registration. Four tests cover happy path, no-run, incomplete
draft, default-loadout fallback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:40:26 -04:00
gamer147
b1397e3a3e refactor(repositories): move static caches into IMemoryCache, enable within-fixture parallelism
BattlePassRepository._curveCache and MissionCatalogRepository._maxLevelCache
were private-static fields populated lazily on first read from whatever
DbContext happened to be in scope. In production "one DbContext lineage
per process" makes that fine. Under parallel test execution each
SVSimTestFactory owns its own SQLite :memory: DB, so the first reader's
DB (often empty, in tests that don't seed BP) poisoned the cache for
concurrent readers from a seeded DB — assertions like "BP level info
must be present after seeding" failed because the process-static cache
returned an empty list populated by the other test's empty DB.

The first patch attempted a `BypassCacheForTests` static flag, which is
exactly the kind of test-only seam that rots the production code: future
caches get the same flag, repos accumulate hidden knobs, and the
underlying invariant ("a cache populated from arbitrary scope serves
arbitrary scope") goes unaddressed.

Instead, move both caches into the DI-registered IMemoryCache.
AddMemoryCache() registers it as singleton-per-service-provider:
production has one provider → one IMemoryCache → identical caching
semantics to before. Each WebApplicationFactory builds its own
provider → its own IMemoryCache → cache is naturally scoped per fixture,
no cross-test bleed possible.

The ResetLevelCurveCache() method and its three call sites
(SVSimTestFactory.SeedGlobalsAsync, BattlePassServiceTests,
LoadControllerTests) are deleted — a fresh factory owns a fresh empty
cache, no manual invalidation needed.

With this and the previous StoryService fixture-instance fix in place,
ParallelScope.All works: 776/776 in 57s wall clock (down from 59s on
Fixtures, 2m13s pre-parallelism).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 00:41:09 -04:00
gamer147
2c62a7be80 refactor(inventory): delete old primitives after InventoryService cutover
Removed RewardGrantService, CurrencySpendService, ICurrencySpendService,
ViewerEntitlements, IViewerEntitlements, CardAcquisitionService,
ICardAcquisitionService, CardGrantResult and their tests
(RewardGrantServiceTests, CurrencySpendServiceTests,
CardAcquisitionServiceTests, ViewerEntitlementsTests). Removed four DI
registrations from Program.cs. No caller references any deleted type;
GrantedReward and EffectiveCosmetics were pre-moved to InventoryGrantTypes.cs
in the prior commit. Build clean, 712/712 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 17:07:30 -04:00
gamer147
c37c04c1b7 refactor(gacha-point): route TryExchangeAsync through IInventoryTransaction
Change signature from (Viewer, packId, cardId) to (IInventoryTransaction, packId, cardId).
Drop RewardGrantService from GachaPointService ctor. PackController.ExchangeGachaPoint opens
tx with GachaPointBalances/Received extra includes, passes tx, commits on success.
Update GachaPointServiceTests to use inv.BeginAsync + tx pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:55:08 -04:00
gamer147
b6bf9b7495 refactor(arena-two-pick): route entry/finish through InventoryService
Replace RewardGrantService + ICurrencySpendService + IViewerEntitlements with
IInventoryService. tx.IsFreeplay replaces FakeEntitlements.IsFreeplay; debit
helpers take IInventoryTransaction. ComputePostStateRewardList deleted (replaced
by result.RewardList from CommitAsync). Update 5 test files to new 8-arg ctor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:51:03 -04:00
gamer147
91909c5755 feat(inventory): read-side methods on IInventoryService + tx
EffectiveBalance/OwnsCard/OwnsCosmetic on the tx are freeplay-aware
against the live viewer. EffectiveOwnedCardsAsync/EffectiveCosmeticsAsync
on the service mirror today's ViewerEntitlements projections (used by
/load/index).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:05:35 -04:00
gamer147
ea340cde21 test(inventory): lifecycle — dispose rollback + use-after-commit
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:03:06 -04:00
gamer147
b0b9901c42 feat(inventory): CommitAsync + currency-collision rule
Last post-state per currency wins; non-currency grants collapse to final
count per (type, id). Deltas are verbatim queued, no cascade. SaveChanges
+ DB tx commit happen atomically inside Commit; failure leaves rollback
to DisposeAsync. CS0649 warning on _committed is now resolved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:02:39 -04:00
gamer147
1ba3f57709 feat(inventory): BackfillCardCosmeticsAsync
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:01:15 -04:00
gamer147
46d8239d5a feat(inventory): TryDebitAsync dispatches currencies + Item
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:00:24 -04:00
gamer147
301da9eeca feat(inventory): TrySpendAsync covers all 4 wallets + freeplay
Crystal/Rupy/RedEther freeplay no-op (returns configured amount,
balance unchanged); SpotPoint always real. Insufficient returns
current balance; success returns post-deduction balance.
SVSimTestFactory gains freeplayEnabled ctor overload that upserts
the Freeplay GameConfigSection row after EnsureSeedData.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:59:26 -04:00
gamer147
a821b7f6b4 feat(inventory): GrantAsync handles Card + cosmetic cascade
Card grants produce a post-state-total entry and run the CardCosmeticReward
cascade (foil twin → id-1 lookup). Cascade additions are skipped when the
viewer already owns the cosmetic; missing-master-row failures are logged
and dropped without failing the parent grant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:54:36 -04:00
gamer147
1f3f81d878 feat(inventory): GrantAsync handles Item branch
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:52:42 -04:00
gamer147
a1cf1d7519 feat(inventory): GrantAsync handles cosmetic branches
Sleeve/Emblem/Skin/Degree/MyPageBG grants are idempotent on the viewer's
owned-collection but always emit a wire entry at the top level (preserves
"+1 sleeve" purchase popup). Unknown ids throw InventoryCatalogException.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:51:46 -04:00
gamer147
3bc38b407b feat(inventory): GrantAsync handles currency branches
Crystal/Rupy/RedEther/SpotCardPoint grants mutate ViewerCurrency in place
and emit post-state-total wire entries. Op log records the post-state for
later currency-collision resolution in CommitAsync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:50:27 -04:00
gamer147
02e86cf16c feat(inventory): BeginAsync loads viewer with canonical graph
Includes Cards/Sleeves/Emblems/LeaderSkins/Degrees/MyPageBackgrounds/Items
under AsSplitQuery, plus caller-supplied extras via InventoryLoadConfig.
Opens a DB transaction and returns an InventoryTransaction shell. All
mutation methods throw NotImplementedException until subsequent tasks
land them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:46:20 -04:00
gamer147
fc504af496 feat(tk2): weighted-group reward picking
Replaces the all-rows-granted reward model with per-group weighted
pick. Each ArenaTwoPickReward row now belongs to a RewardGroup with a
Weight; finish/retire groups the WinCount's rows by RewardGroup and
picks exactly one row per group, weighted by Weight (excluding
Weight==0). A RewardNum==0 outcome skips both the grant and the
rewards[] emission. Empty WinCount catalogs emit empty arrays.

Existing seed entries preserve deterministic behavior by living in
single-option groups (each with weight 1). Future seasons can expand
groups to multi-option for true randomized rewards (e.g. 200-280
rupies).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 13:44:33 -04:00
gamer147
1e2e18e828 fix(tk2): rewards array uses ReceivedReward shape (reward_detail_id/item_type/is_usable)
The /retire and /finish responses carry two reward arrays with DIFFERENT
key schemas:

  rewards[]      → ReceivedReward(JsonData) parser
                   {reward_type, reward_detail_id, item_type, reward_count?, is_usable}
  reward_list[]  → PlayerStaticData.UpdateHaveUserGoodsNumByJsonData
                   {reward_type, reward_id, reward_num}

We were emitting both with reward_list's schema, so the client threw
KeyNotFoundException on `data["reward_detail_id"]` while parsing each
delta entry — observed live as the retire-screen failure.

- New TwoPickRewardReceivedDto mirrors the existing Achievement/
  TotalReceiveCountDto shape.
- FinishResponseDto.Rewards switched from List<RewardEntryDto>
  to List<TwoPickRewardReceivedDto>.
- GrantRunRewardsAndDeleteAsync pre-loads ItemEntry.Type for any
  Item-typed reward so item_type ships correctly (0 for currencies).
- Existing tests renamed RewardNum→RewardCount on the deltas list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 12:56:05 -04:00
gamer147
6381e4da51 fix(tk2): match original SV (5-battle cap, no loss limit)
User clarified: the 0..7 win reward tiers came from Shadowverse Worlds
Beyond (sequel), not the original game we're emulating. Original SV's
Take Two caps at 5 total battles played and has no loss limit (verified
on prod: queueing continues with 2+ losses).

- arena-two-pick-rewards.json: drop 6w + 7w tiers (12 rows remain).
- ArenaTwoPickConfig: remove MaxLosses property.
- ArenaTwoPickService: termination is now battlesPlayed >= maxBattles
  (5 from MAX(reward.WinCount)). RecordBattleResult no longer flips
  IsSelectCompleted on the 2nd loss.
- ResolveMaxBattleCountAsync empty-catalog default 7 → 5.
- Tests updated for the new counts (16 → 12 rows, max 7 → 5).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 12:47:43 -04:00
gamer147
dc19289818 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>
2026-05-31 12:26:57 -04:00
gamer147
e245d5b158 feat(svc): Retire + Finish + RecordBattleResult
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:10:41 -04:00
gamer147
cc40e2d2e8 feat(svc): ChooseClassAsync + ChooseCardAsync (draft state machine)
Implements the class-selection and card-pick turns for the Take Two arena draft:
- ChooseClassAsync validates class is offered, locks ClassId, generates first pick set via pool
- ChooseCardAsync appends the two picked cards, advances SelectTurn 1–15, completes draft at turn 15
- 6 new tests covering happy paths and all error codes (class_not_offered, invalid_state, invalid_selection)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:03:50 -04:00
gamer147
d550f66481 feat(svc): EntryAsync (ticket debit + run insert + candidate classes)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:59:05 -04:00
gamer147
ba49852c42 feat(svc): IArenaTwoPickService + response DTOs + GetTopAsync
6 response DTOs, IArenaTwoPickService interface + ArenaTwoPickException,
ArenaTwoPickService skeleton with GetTopAsync implemented and stubs for
Tasks 13-15. 3 NUnit tests for GetTopAsync all pass. DI: AddScoped.
2026-05-31 10:51:41 -04:00
gamer147
a98b60dd36 feat(svc): ArenaTwoPickCardPoolService (rarity-weighted, class+neutral)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:46:59 -04:00