84 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
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
gamer147
723a97e3af feat(pack): gacha-point accrual with per-child override 2026-05-28 23:23:33 -04:00
gamer147
e41ceff0be refactor(pack): clarify gacha-point leader detection and drop unused grants injection 2026-05-28 23:20:33 -04:00
gamer147
66c0b1c951 feat(pack): gacha-point catalog read (legendaries + leader cards) 2026-05-28 23:11:48 -04:00
gamer147
261ce67cee fix(story): tutorial section is_finished derives from viewer tutorial_step
Section 0 (prologue) has no chapter rows server-side — the prologue is
hardcoded client-side in Wizard/Prologue.cs — so the chapter-completion
rollup always emitted is_finished=false. The client uses that flag to
derive IsTutorialReplay; with it false, AreaSelectUI.OnTouchChapterListTutorial
blocks every chapter switch and the default focus (last visible chapter)
becomes the only confirmable one, matching the reported "all 3 greyed out,
only the 3rd playable" symptom on replay.

Override sectionFinished for id=0 with viewer.MissionData.TutorialState >= 100,
matching prod traffic_prod_626_story.ndjson btn_story_tutorial.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:01:50 -04:00
gamer147
177b4925a1 fix(session): bound the SID→UDID dict with FIFO eviction
The map used to grow unbounded over the process's lifetime — every fresh
signup added an entry that was never reclaimed. Long-running dev hosts
(or any future emulator deployment that doesn't restart often) would
gradually leak memory. Cap at 10k entries by default with a simple FIFO
eviction queue; re-stores of the same SID don't grow the queue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:13:47 -04:00
gamer147
27ebb5114c fix(pack): tutorial flow display + open end-to-end
Four targeted fixes that together let /tutorial/pack_info display
the legendary starter at index 0, let /tutorial/pack_open succeed
on it, and let the pack drop out of the shop after.

1. /pack/info now loads viewer.Items into a Dictionary<long,int>
   and threads it through ToDto so child_gacha_info.item_number
   reflects the viewer's actual owned count of item_id. Previously
   defaulted to 0 for every pack, so the legendary pack 99047
   reported item_number=0 immediately after the gift granted 1×
   ticket id=90001. Verified against the prod tutorial capture.

2. PackRepository.GetActivePacks now orders parent_gacha_id DESC
   to match prod's /pack/info wire order (99047, 92001, 80047,
   16015...10001). The tutorial pack UI runs with controls locked
   and auto-selects index 0 via GachaUI.GetCurrentLegendPackId
   (FirstOrDefault on IsLegendPackId), so the legendary starter
   needs to be the first legend pack in the list.

3. DbCardPoolProvider.GetPool falls back to all in-rotation cards
   when a LegendCardPack's base set has no rows. Pack 99047's
   base_pack_id is 90001, a synthetic "Throwback Rotation" category
   that doesn't correspond to a real card_set in the prod card
   master — its real pool is curated across older rotation sets
   (Altersphere through Colosseum). We don't have that membership
   map captured yet; the rotation fallback is broader than prod
   but produces a valid 8-card draw, which is what the tutorial
   needs to advance to step 100. TODO in code points at the
   real fix.

4. PackController.Open's tutorial path now consumes the granted
   ticket (decrement viewer.Items by packNumber for child.ItemId)
   and emits the post-state count in reward_list as
   {reward_type:4, reward_id:item_id, reward_num:post_count}.
   Without this, the pack stayed at item_number=1 forever, the
   shop kept showing it post-tutorial, and the next click hit
   /pack/open (not /tutorial/pack_open) which 501s on type_detail=5.

Also: docstring on PackConfigDto.SalesPeriodInfo flags the deferred
wire-fidelity fix (prod emits {"sales_period_time": "<complete_date>"}
for limited windows, [] for evergreens; we always emit {}) and the
retype from Dictionary<string,string?> to a typed
PackSalesPeriodInfoDto. Doesn't affect tutorial flow, deferred for
the pack-system rework.

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