Commit Graph

152 Commits

Author SHA1 Message Date
gamer147
8e017c9d10 feat(check): stub /check/check_time_slip_card_master_hash
Bare BaseTask call fired from DeckDecisionUI.cs:140 (Arena "View Deck"
path) and the TK2 prep screen. Client task has no Parse() override —
just checks result_code, ignores body. Prod (4 captured instances
across traffic_prod_taketwo_selections + traffic_prod_tradeables_capture)
unanimously responds with data: [].

Routing smoke added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 13:29:27 -04:00
gamer147
ac2f31103d fix(arena): match prod get_challenge_info wire shape; stub ranking_history
Prod /arena/get_challenge_info capture (Season 26):
- reward_step_info.reward_step_list is a Dict<string,string>
  ({"5":"5","10":"10","15":"15"}), not the List<int> I'd assumed
- max_reward_step is stringified

The previous stub would have parsed at the client (LitJson tolerates the
shape via indexed iteration), but cleaning to match prod exactly.

Also stubs /arena/get_challenge_ranking_history (new endpoint observed
in the same capture). Prod ships {two_pick: [], sealed: []} with no
history populated — empty lists match. Routing smoke added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 13:21:44 -04:00
gamer147
1af56b4ec4 fix(tk2): per-viewer is_join in arena_info + stub /arena/get_challenge_info
Bug 1 ("pay to enter again after restart"):
arena_info[0].is_join shipped from the static ArenaSeasonConfig seed,
so /load/index and /mypage/index always emitted false regardless of
viewer state. The client uses is_join to choose between the "Pay to
enter" and "Resume run" dialogs (Wizard/ChallengeEntry.cs:165 + the
ArenaEntryBase._isJoinFunc pivot). Without a per-viewer override every
cold start after a partial run looked like "no run" and the player got
charged again.

LoadController + MyPageController now compute is_join from
ViewerArenaTwoPickRuns presence. MyPageController grew an
IArenaTwoPickRunRepository dep (LoadController already had _db).

Bug 2: /arena/get_challenge_info 404. Stubbed via a new
ArenaController + DTO pair. Returns the season seed's begin/end_time
+ name where available; placeholder zeros for win history. All 6 keys
required by ChallangeHistoryInfoTask.Parse are present (unconditional
JsonData lookups).

Routing smoke added for /arena/get_challenge_info.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 13:13:11 -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
668779e8a4 fix(http): inherit BaseRequest on all TK2 + Colosseum request DTOs
MessagePack [Key("...")]-keyed contracts reject unknown fields, so request
DTOs that omit BaseRequest's envelope (viewer_id, steam_id,
steam_session_ticket) fail deserialization on the real msgpack wire path.
Routing smoke + JSON-direct tests didn't catch this because S.T.J. tolerates
extra keys and the routing smoke uses ValidBaseRequestJson, but anything
sent via the actual client encrypted=True path threw
MessagePackSerializationException.

Fix: every Arena*Request now inherits BaseRequest. Also updates the JSON
controller tests + e2e to include the envelope so the [ApiController]
auto-400 validation passes.

Discovered via /arena_colosseum/get_fee_info crash on the in-game arena
screen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 12:06:50 -04:00
gamer147
f8ca4a0ae9 feat(http): stub /arena_colosseum/get_fee_info (is_colosseum_period:false) 2026-05-31 11:58:18 -04:00
gamer147
98fb3c5fcd fix(svc): default MaxBattleCount=7 with warn-log on empty reward catalog 2026-05-31 11:41:57 -04:00
gamer147
09b8c49743 feat(http): ArenaTwoPickBattleController (do_matching stub + finish)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:27:02 -04:00
gamer147
f272690a31 feat(http): ArenaTwoPickController (6 actions)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:15:53 -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
gamer147
30a723322c feat(dto): TK2 common DTOs (entry/class/deck/candidate/results/reward)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:40:06 -04:00
gamer147
2df18425c4 feat(repo): IArenaTwoPickRunRepository + tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:37:42 -04:00
gamer147
721cd738d7 feat(repo): IArenaTwoPickRewardRepository + tests
Adds GetRewardsByWinCountAsync and GetMaxWinCountAsync (short-circuits
to 0 on empty table). Registers as AddTransient in Program.cs alongside
other global catalog repos. 3 NUnit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:34:12 -04:00
gamer147
50e4989b77 docs(importers): update data_dumps path references for reorg
Mirror of the outer-repo data_dumps/ reorganization (commit e1e595d in
the SVSim outer repo): updates all data_dumps/extract/ → data_dumps/scripts/,
data_dumps/client_master_csv → data_dumps/client-assets, data_dumps/traffic
→ data_dumps/captures/traffic in XML doc-comments and inline comments
across importers, controllers, middlewares, DTOs, and tests. Doc-only;
no logic changes; build green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 01:22:08 -04:00
gamer147
1470406e17 fix(gacha-points): include IsLeader cards regardless of draw tier
Prod's /pack/get_gacha_point_rewards offers leader cards from packs
where the leader sits in a non-Legendary tier — UCL pack 16015 has
Kyoka (711531010, Runecraft) and Miyako (711331010, Dragoncraft) as
Gold-tier rows with is_leader=1 in the drawrates. The old filter
(Tier == Legendary && !IsAltArt) excluded them, so the in-game
exchange UI was empty despite the banner advertising leader-card draw
rates.

Fix: filter on (Tier == Legendary || IsLeader) && !IsAltArt. Captures
every legendary plus any leader card regardless of page tier. Verified
against the captured 16015 response in
traffic_prod_all_gacha_exchange.ndjson (28 entries: 26 legendaries +
2 Gold-tier leaders).

Across the seeded data this surfaces 6 additional cards: 3 Bronze-tier
leaders + 3 Gold-tier leaders. The 68 Legendary-tier and 81 Special-
tier leaders were already included.

Renames legendaryCardIds -> exchangeableCardIds for clarity.

Regression test seeds a Gold-tier IsLeader=true card with a Skin row
and asserts the exchange catalog returns it with the Skin reward
entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 00:21:42 -04:00
gamer147
61ae086332 fix(gacha-points): look up by odds_gacha_id, not parent_gacha_id
The two wire fields differ for seasonal packs (verified against
traffic_prod_all_gacha_exchange.ndjson — every captured request pairs
odds_gacha_id=16xxx with parent_gacha_id=10xxx). The OLD DTO docstring
assumed they were always equal; today's controller used
ParentGachaId, which lands on the base/family pack id (often a
synthesized disabled stub with no GachaPointConfig) and returns [].

Fix:
- GetGachaPointRewards and ExchangeGachaPoint now consume OddsGachaId.
- Update both DTO docstrings to document the seasonal-pack pattern.
- Regression test seeds (16015 enabled w/ GachaPointConfig, 10015
  disabled stub w/o config) and asserts the response uses 16015's
  catalog.

Symptom: opening pack 16015 (parent_gacha_id=16015 in /pack/open)
accrued gacha points correctly, but /pack/get_gacha_point_rewards with
{odds_gacha_id:16015, parent_gacha_id:10015} returned an empty list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:30:18 -04:00
gamer147
9c9d0fc41f feat(packs): accept all currently-supported currencies on /pack/open
Extends /pack/open beyond the v1 CRYSTAL_MULTI=2 / DAILY=3 / RUPY_MULTI=7
trio to cover every type_detail whose payment primitive already exists:

  1 CRYSTAL              -> ICurrencySpendService crystal debit
  6 RUPY                 -> ICurrencySpendService rupee debit
  4 TICKET / 5 TICKET_MULTI -> debit child.ItemId from OwnedItemEntry
                            (ticketsNeeded = cost * packNumber), 400 on
                            missing/short balance; reward_list gets a
                            RewardType=4 post-state Item entry to mirror
                            project_wire_reward_list_post_state

Skin-overload type_details (8/9/13) and free-pack overlays (10/11/12)
stay 501 — they need selection / banner plumbing the current code
doesn't have.

Tutorial alias unchanged: it still consumes the gating ticket post-draw
and stamps tutorial_step=100. The two ticket flows diverge by intent
(tutorial = free server-grant; normal = paid by ticket inventory).

Removed Open_rejects_ticket_type_detail (asserted the old 501 path);
covered by Open_rejects_insufficient_tickets. Updated
NonTutorial_pack_open_does_not_emit_tutorial_step to assert the new
200-on-ticket-success behavior — same invariant under test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:17:11 -04:00
gamer147
517f855112 feat(packs): wire PackDrawTableImporter; retire ICardPoolProvider
Bootstrap Program.cs now calls PackDrawTableImporter after PackImporter.
Delete DbCardPoolProvider, ICardPoolProvider, and the DbCardPoolProvider
tests — the new IPackDrawTableRepository covers what GachaPointService
needed (legendary-tier card_ids per pack) and PackOpenService takes the
draw table directly.

GachaPointService now resolves the legendary catalog from
PackDrawTable.CardWeights filtered by Tier==Legendary, instead of
ICardPoolProvider.GetPool then a rarity filter. Same end set, no DB pool
walk.

Test fallout: tests that fabricate custom card sets for gacha-point
tests now call factory.SeedPackDrawTableFromSetAsync(packId, setId)
to install a matching legendary-tier stub. Full suite: 647/647 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 22:45:02 -04:00
gamer147
1c386b5ed0 feat(packs): rewrite PackOpenService against per-pack draw table
Sampler is now driven by PackDrawTable: roll DrawTier per slot by
cumulative slot-rate weights, then pick a card within tier by per-card
weights renormalized within the tier. Rate-less Guaranteed-Leader-Card
rows draw uniform over (pool minus owned), falling back to the full
pool when all are owned. Bonus slot fires once at the end of a 10-pack
open when HasBonusSlot is set.

Slot 8 falls back to the general slot's per-card weights for the rolled
tier when slot-8 has only a rarity-level rate quoted (the common shape
on normal packs).

PackController.Open loads the draw table + viewer owned card ids and
passes them to the sampler; the category-based forced-Legendary slot-8
override is gone. ICardFoilLookup replaces ICardPoolProvider for the
foil-twin heuristic.

Drops the test-fixture pack-draw seed overlay so the production seed
flows through the importer tests; controller tests that fabricate their
own card sets now call factory.SeedPackDrawTableAsync(...) to install a
matching stub draw table.

WeightedPick helper handles the cumulative-band roll for both stages.
Five sampler tests + four WeightedPick tests + five importer/repo
tests; full suite is 653/653 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 22:26:45 -04:00
gamer147
0169ec57b4 feat(packs): split TryGetFoilTwin into ICardFoilLookup
Extracts the foil-twin lookup from ICardPoolProvider into a dedicated
ICardFoilLookup service. PackOpenService takes the lookup as a
parameter; the legacy DbCardPoolProvider stays registered until T12
removes it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:53:59 -04:00
gamer147
3c36124fa7 feat(packs): PackDrawTable aggregate + IPackDrawTableRepository
Aggregate (Config + SlotRates + CardWeights) and a single-pack getter
loaded as one unit per /pack/open. PackOpenService consumes the
aggregate; tests use the production seed (fixture overlay) to validate
shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:53:33 -04:00
gamer147
f754ef1ad3 fix(import): tolerate numeric my_rotation_id; skip empty deck slots
A real /load/index dump emits my_rotation_id as a bare number (0) for
unset MyRotation slots, which 400'd against the string? DTO field
(AllowReadingFromString only covers string->number). FlexibleStringConverter
accepts either form. Also skip empty deck slots (no cards) on import — a
dump carries every slot, mostly empty placeholders the client manages
itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 21:03:10 -04:00
gamer147
2e96001654 docs(import): update DefaultSleeveId comment after removing deck cloning 2026-05-29 18:54:00 -04:00
gamer147
4965851238 feat(import): import decks; remove obsolete default-deck cloning
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 18:42:07 -04:00
gamer147
d7e5557d61 feat(import): import consumable item inventory 2026-05-29 18:33:11 -04:00
gamer147
71b3c3e19f feat(import): import owned card collection with unknown-card skip
Extends POST /admin/import_viewer to accept owned_cards (snapshot full-replace).
Unknown card_ids are skipped, counted in skipped_card_count on the response, and
logged server-side. Count is clamped to OwnedCardEntry.MaxCopies (3). Also injects
ILogger into AdminController and adds Cards/Items/Decks to the viewer reload graph.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:22:44 -04:00
gamer147
302bf17c31 feat(cosmetics): route ownership checks + shop owned-flags through entitlements (freeplay)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:36:50 -04:00
gamer147
d68a85bbc5 refactor(battlepass): route premium-buy crystal spend through CurrencySpendService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:23:50 -04:00
gamer147
ee407befb5 refactor(spotcard): centralize spot-point spend via CurrencySpendService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:20:32 -04:00
gamer147
5c6b703276 refactor(itempurchase): route currency spend (not items) through CurrencySpendService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:18:57 -04:00
gamer147
fb257a544f refactor(leaderskin): route currency spend through CurrencySpendService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:17:01 -04:00
gamer147
1f58461326 refactor(sleeve): route currency spend through CurrencySpendService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:14:14 -04:00
gamer147
2e021c8b9e refactor(builddeck): route currency spend through CurrencySpendService
Inject ICurrencySpendService and replace the inline crystal/rupee debit
block in BuildDeckController.Buy with TrySpendAsync calls, so freeplay
mode gets the no-deduct path automatically. All 18 BuildDeckController
tests pass unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:12:43 -04:00
gamer147
163299504a refactor(pack): route currency spend through CurrencySpendService (freeplay) 2026-05-29 14:10:50 -04:00
gamer147
a3a49077b5 refactor(load): drop now-dead card/collection repo deps after entitlements move
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:07:30 -04:00
gamer147
092176ea1a feat(load): project currency/cards/cosmetics through entitlements (freeplay)
Route /load/index currency, owned-card list, and cosmetic id-lists through
IViewerEntitlements so freeplay mode inflates all three without touching
the viewer's DB state. Adds LoadControllerFreeplayTests (2 tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:03:35 -04:00
gamer147
d560f9ade4 chore(di): register entitlements + spend services; add test freeplay helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 13:55:46 -04:00
gamer147
2d675aa35d feat(practice): serve default/trial/leader-skin lists on practice/deck_list
practice/deck_list returns the same wire shape as /deck/info (the client parses
both via DeckGroupListData), but only ever sent user decks — so a fresh account
saw no default decks and couldn't start a practice match.

Extract the /deck/info hydration into a shared IDeckListBuilder used by
/deck/info, /deck/my_list, and /practice/deck_list. Practice passes
padEmptySlots:false (deck *select*, not builder) — matches the prod practice
capture, which returns real decks unpadded plus the 8 per-class default decks
and per-class leader-skin settings. Retire the near-duplicate
PracticeDeckListResponse DTO in favor of the shared DeckListResponse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:01:36 -04:00
gamer147
6f9976ebad style(story): blank line before StartAsync 2026-05-29 11:11:40 -04:00
gamer147
bd2eaa9e97 refactor(deck): re-type /deck/info trial_deck_list to List<TrialDeck>?
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:54:53 -04:00
gamer147
66dc0cc657 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>
2026-05-29 10:47:20 -04:00
gamer147
6a507553d1 feat(dto): TrialDeck + fleshed BuildDeck + trial/default on GetDeckListResponse 2026-05-29 10:38:39 -04:00
gamer147
7292c44082 fix(pack): include all pack legendaries in gacha-point catalog + correct class_id 2026-05-29 08:36:37 -04:00
gamer147
a8bbc39bfd fix(pack): emit one gacha-point entry per emblem cosmetic + clean stale docstring 2026-05-29 00:39:50 -04:00