762 Commits

Author SHA1 Message Date
gamer147
72c8fe627b feat(packs): PackDrawTableImporter with fixture tests
Idempotent upsert keyed on pack_id; slot rates and card weights are
wiped per pack and reinserted. String slot/tier in the seed translate
to enum at import time.

Tests:
- Imports_config_slot_rates_and_card_weights
- Is_idempotent_on_rerun

Fixtures live under SVSim.Bootstrap/Data/test-fixtures/seeds/ and overlay
the production seeds via the existing csproj rule (test-fixture file
beats production file at same Link path).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:45:06 -04:00
gamer147
f9f5b0dfa4 feat(packs): add PackDraw seed DTOs and IsEnabled on PackSeed
Three new seed DTOs (PackDrawConfigSeed, PackDrawSlotRateSeed,
PackDrawCardWeightSeed) — slot and tier carried as strings, importer
translates to enum.

PackSeed gains is_enabled (defaults true so existing seeds remain
enabled).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:43:27 -04:00
gamer147
8e98180951 feat(packs): add pack-draw and pack-stub seed files
Generated by data_dumps/extract/extract-pack-draw-rates.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:42:20 -04:00
gamer147
b78d7d6cbe feat(packs): add PackDraw* tables and IsEnabled column
Three new EF entities and a migration:
- PackDrawConfigEntry (per-pack: animation rate, has-bonus flag, special-kind label)
- PackDrawSlotRateEntry (pack/slot/tier -> rate, unique index)
- PackDrawCardWeightEntry (per-card-rate facts incl rate-less rows)

DrawSlot {General, Eighth, Bonus} and DrawTier {Bronze, Silver, Gold, Legendary, Special}.
Special collapses leader_card and limited_time_leader (verified mutually exclusive per pack).

IsEnabled column on PackConfigEntry — admin gate for synthesized stubs, distinct from
the wire-mirror IsHide.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:40:50 -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
06108e4b6f test(import): literal-client-JSON wire-shape coverage for new fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:57:54 -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
ed5be80f08 Merge freeplay-mode: global freeplay toggle + centralized spend/entitlement primitives 2026-05-29 16:40:46 -04:00
gamer147
9b2696fac5 test(freeplay): assert DB-untouched invariant on freeplay pack open
Crystal-pack open under freeplay with 0 balance: verifies the request
succeeds (HTTP 200) and Currency.Crystals is unchanged in the DB afterward.
2026-05-29 14:42:10 -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
0052307686 feat(services): CurrencySpendService (central debit primitive, freeplay-aware) 2026-05-29 13:49:36 -04:00
gamer147
b7ee0cdcf8 test(entitlements): cover EffectiveOwnedCards/Cosmetics; document include preconditions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 13:47:22 -04:00
gamer147
3bf9ad1c42 test(config): include Freeplay in exhaustive ConfigSection seed-count assertion 2026-05-29 13:41:52 -04:00
gamer147
91c539fb8d feat(services): ViewerEntitlements (freeplay-aware ownership/balance authority)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 13:40:24 -04:00
gamer147
be19c0ad8d feat(repo): cosmetic catalog id enumerations on ICollectionRepository 2026-05-29 13:29:19 -04:00
gamer147
b4f6992918 feat(services): declare entitlements + currency-spend primitives
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 13:27:22 -04:00
gamer147
7b5edb7c65 feat(config): add Freeplay config section (default off)
Adds FreeplayConfig [ConfigSection("Freeplay")] with Enabled=false,
CurrencyAmount=99999, CardCopies=3, and ShippedDefaults(). Covered by
FreeplayConfigTests verifying the tier-chain resolves shipped defaults.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 13:25:47 -04:00
gamer147
76aad36e84 Merge practice-default-decks: serve default/trial/leader-skin lists on practice/deck_list 2026-05-29 12:12:38 -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
1e53748ae3 Merge story-build-trial-decks: serve build/trial/default deck lists on get_deck_list 2026-05-29 11:22:05 -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
363213ccf7 test(story): literal-JSON wire-shape guard for get_deck_list deck lists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:52:06 -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
68d783192d feat(repo): GetStoryDecksByClass joins story-deck presentation to product card lists
Adds StoryDeckView projection, IBuildDeckRepository.GetStoryDecksByClass interface method,
and BuildDeckRepository implementation that loads StoryDeckEntry rows for a class, fetches
matching BuildDeckProductEntry card lists, and expands each card by Number into a flat
CardIdArray. TDD: 2 tests in StoryDeckRepositoryTests (expand + empty-class).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:36:14 -04:00
gamer147
e792e8d79d feat(bootstrap): StoryDeckImporter + seed model, wired after BuildDeck
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:32:12 -04:00
gamer147
e0da7f09ca feat(db): AddStoryDeck migration (DDL)
Creates StoryDecks table with all required columns (Id PK, DeckNo, Kind,
ClassId, DeckName, SleeveId, LeaderSkinId, IsRecommend, OrderNum, EntryNo,
DeckFormat nullable, DateCreated, DateUpdated). Pure DDL — no InsertData/HasData.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:28:50 -04:00
gamer147
75a2fca8bb feat(db): StoryDeckEntry presentation table + StoryDeckKind enum
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:25:03 -04:00
gamer147
405f49c490 feat(seeds): story-deck presentation seed (53 build + 59 trial)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:23:11 -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
168e347a82 feat(pack): wire real gacha-point balance into /pack/info (skip ticket-only packs) 2026-05-29 00:16:08 -04:00
gamer147
739f629996 feat(pack): accrue gacha points on /pack/open (skip tutorial) 2026-05-29 00:07:28 -04:00
gamer147
b47ec3b64d feat(pack): /pack/exchange_gacha_point endpoint
Wires IGachaPointService.TryExchangeAsync into a controller endpoint.
Loads the viewer with the full cosmetic-grant graph (Cards, Sleeves,
Emblems, Degrees, LeaderSkins, MyPageBackgrounds) plus the gacha-point
balance + received marker, with AsSplitQuery to avoid the cartesian
explosion documented in project_ef_split_query. Returns BadRequest with
the outcome's error code on validation failure, or the post-state
reward_list on success.

Two integration tests: happy-path verifies card grant + balance debit +
received-marker persistence + post-state reward_list shape; insufficient-
balance path returns 400.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 23:57:39 -04:00
gamer147
9e7b7eed27 feat(pack): /pack/get_gacha_point_rewards endpoint 2026-05-28 23:49:08 -04:00
gamer147
1eaf0d0bc4 refactor(pack): collapse null-forgive in gacha-point exchange balance guard 2026-05-28 23:42:32 -04:00