Commit Graph

90 Commits

Author SHA1 Message Date
gamer147
5083d43e51 feat: ILoginBonusService — JST day-bucketed claim + grant + DTO assembly
GrantIfDueAsync advances streak (1..15 cycle), grants the day's reward via
inventory tx, returns wire-shape DailyLoginBonus. IsDue helper for MyPage.

Also fixes GameConfigurationJsonbTests section-count (15→16) which was broken
since Task 3 added the LoginBonus [ConfigSection] but the assertion wasn't updated.
2026-06-13 16:15:21 -04:00
gamer147
d126649ad4 feat(arena-colosseum): 2-pick + curated deck sources (phase 3)
Closes the family. arena-colosseum 10/16 → 15/16 zero stubs (the 16th —
finish_load — is dead per project_dead_battle_endpoints).

* 2-Pick draft lift onto ArenaColosseumController:
  - get_candidate_classes samples from ArenaTwoPickConfig.AllowedClassIds
    and persists the slate onto the run.
  - class_choose accepts class_id XOR chaos_id; both populate run.ClassId,
    chaos branch stores ChaosId for replay.
  - get_candidate_cards is the idempotent draft-resume snapshot.
  - card_choose appends both cards from the picked pair, advances turn 1..15.
  - Pool override: ArenaTwoPickCardPoolService gets a non-breaking
    GeneratePickSetsForTurn(..., poolCardSetIds) overload; Colosseum routes
    pass ColosseumSeasonConfig.PoolCardSetIds (falls back to challenge →
    rotation when empty).
* Curated-deck schema: ColosseumHofDeck / ColosseumWindFallDeck /
  ColosseumAvatarDeck — three identical tables (separate per per-pool
  operational lifecycle), unique on DeckNo. Migration AddColosseumCuratedDecks.
* IColosseumCuratedDeck interface + a generic ColosseumCuratedDeckImporterBase<T>
  with three concrete subclasses (HOF / WindFall / Avatar), registered in
  Bootstrap. Seed files ship empty.
* 6 curated endpoints on ArenaColosseumController: get_{hof|windfall|avatar}_deck_list
  return BARE-ARRAY data per spec; register_{hof|windfall|avatar}_deck share
  one generic RegisterCuratedAsync<T> dispatcher. Cross-pool register
  rejected via per-pool lookup. Curated register has no is_published flag
  (constructed-only) — clears the run's flag for state consistency.
* Tests: 5 draft HTTP tests + 11 curated-deck HTTP tests (4 parameterized
  3 ways across HOF/WindFall/Avatar + a cross-pool isolation test + an
  is_published clear test). Existing TwoPick service tests updated for the
  new pool overload. Full suite: 1347/1347.

Phase 3 ship gate met. Branch ready for merge.
2026-06-13 12:49:57 -04:00
gamer147
cbee0f9a50 feat(arena-colosseum): playable bracket + rank-match promotion (phase 2)
Closes 4 more arena-colosseum spec shapes (6/16 → 10/16): the dual-prefix
battle URLs (colosseum_battle/* and colosseum_rank_battle/*) for both
do_matching and per-match finish, plus the bracket-end /finish + /retire
pair on /arena_colosseum.

* ColosseumProgressionService: pure-logic decisions for win-threshold
  advancement, 3008 promotion trigger, and per-round reward bundles.
  Extends ColosseumRoundsConfig with FinishRewards/RetireRewards per
  round + a top-level ChampionRewards list.
* MatchContextBuilder.BuildForColosseumAsync: lifts the registered deck
  via IDeckRepository (NOT viewer-graph) per project_ef_nav_include_pitfall.
* ArenaColosseumBattleController: dual route prefixes off one controller
  via explicit absolute [HttpPost("/...")] routes — does NOT extend
  SVSimController's [Route("[controller]")]. Same pattern as FreeBattle.
  URL-vs-IsRankMatching mismatch returns 400 with a phase-mismatch error;
  no-deck triggers the standard 3001 matchmaking-illegal state. 3008
  flips run.IsRankMatching exactly once via the progression service.
* /arena_colosseum/finish + /retire share FinishResponse (rewards +
  reward_list + colosseum_status). Champion finish appends ChampionRewards
  and surfaces colosseum_status.is_champion = true. Both delete the run
  row after granting via IInventoryTransaction.GrantAsync.
* DI: ModePolicy entries for "colosseum_battle" and "colosseum_rank_battle".
* Tests: 8 progression-service unit tests (pure logic), 5 battle-controller
  HTTP tests, 5 bracket-terminate HTTP tests, 2 full-bracket E2E walks
  (champion path + retire mid-round). Full suite: 1331/1331.
2026-06-13 12:32:56 -04:00
gamer147
715dd4dea8 feat(config): wire /config/update + /config/update_challenge_config (closes family 4/4)
is_skip_gacha_effect, use_challenge_two_pick_premium_card, and
challenge_two_pick_sleeve_id all persist on ViewerInfo and round-trip
through /load/index.

The two challenge fields move from the global ChallengeConfig section to
ViewerInfo — they're viewer preferences, not server-wide policy. Premium
card defaults to 0; sleeve falls back to DefaultLoadoutConfig.SleeveId
(3000011) when unset. MatchContextBuilder's TK2 sleeve read switches to
the same viewer-scoped path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 11:25:40 -04:00
gamer147
014cd62729 fix(card-master): degrade payload provider gracefully on IO error or empty file 2026-06-12 12:03:32 -04:00
gamer147
02cc8537f2 feat(card-master): add ICardMasterPayloadProvider singleton, load blob at startup 2026-06-12 11:59:35 -04:00
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
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
259e3ebe29 feat(2pick): tag finish-reward tx as ArenaTwoPickFinish for acquire history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:05:37 -04:00
gamer147
c72b692560 feat(battle-pass): tag claim tx as BattlePassClaim for acquire history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:05:09 -04:00
gamer147
d767944a83 feat(story): tag finish-reward tx as StoryFinish for acquire history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:04:11 -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
a033bf361a fix(battle-pass): remove redundant SaveChanges after CommitAsync
CommitAsync's inner SaveChangesAsync already flushes the AddClaim
rows + progress.IsPremium mutation alongside the inventory grants
(same scoped DbContext). The trailing _db.SaveChangesAsync was a
no-op in BuyPremium and only meaningful in AddPoints when no level
crossed (no tx opened) — restructured to an else branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 18:48:26 -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
26bc4fe2ab refactor(battle-pass): route BuyPremiumAsync and AddPointsAsync through InventoryService
Replace RewardGrantService + ICurrencySpendService with IInventoryService tx.
CommitAsync's currency-collision rule replaces the manual Crystal RemoveAll+re-append
scrub in BuyPremiumAsync. AddPointsAsync uses result.Deltas for NewlyClaimed to
preserve per-track visibility (two Rupy grants stay two entries).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:46:13 -04:00
gamer147
7c4bc2966f refactor(story): route FinishAsync rewards through InventoryService
Replace RewardGrantService with IInventoryService tx. Per-reward GrantAsync
calls inside try/catch preserve the NotSupportedException skip; CommitAsync
returns result.RewardList (post-state totals) and accumulated delta list feeds
story_reward_list. Update StoryServiceTests to inject IInventoryService.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:42:38 -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
98fb3c5fcd fix(svc): default MaxBattleCount=7 with warn-log on empty reward catalog 2026-05-31 11:41: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
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
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
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
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
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
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
gamer147
1eaf0d0bc4 refactor(pack): collapse null-forgive in gacha-point exchange balance guard 2026-05-28 23:42:32 -04:00
gamer147
e1f5b9b6c3 feat(pack): gacha-point exchange (debit + grant)
Implements GachaPointService.TryExchangeAsync: validates pack
exchangeability, balance >= threshold, card in catalog, not already
received; debits balance, marks received, grants the card through
RewardGrantService (cascade handles cosmetics). Re-adds the
RewardGrantService injection that was removed in the Task 3 fix-up
(matches the "inject when you call" convention).

Card grant produces the wire-shape reward_list directly via the
cosmetic cascade — the catalog's reward_list remains the display-only
shape for /pack/get_gacha_point_rewards.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 23:37:00 -04:00