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>
PackImporter now runs a second pass over pack-stubs.json, inserting
PackConfigEntry placeholders for any pack_id NOT already present from
the live-capture packs.json pass. Synthesized stubs default
IsEnabled=false; live-capture rows default IsEnabled=true.
PackRepository.GetActivePacks filters by IsEnabled in addition to the
date window, so synthesized stubs stay hidden until an operator opts
them in (UPDATE Packs SET IsEnabled=true WHERE Id=...).
Bundles Task 6 + Task 11 because adding pack-stubs.json to the
test-fixture set surfaces an extra row in PackControllerFullCatalogTests'
35-pack count assertion; the filter is what makes the test resilient.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Stack [HttpPost("/tutorial/pack_open")] alias on PackController.Open. Detect
isTutorialPath via HttpContext.Request.Path; gate the type_detail rejection,
currency switch, open-count tracking, and currency reward_list entries behind
!isTutorialPath so the starter legendary pack (99047/990047, type_detail=5)
bypasses the purchasable-pack code path. After grant, set MissionData.TutorialState=100
and emit tutorial_step=100 in PackOpenResponse — this is the sole END transition,
per live-traffic capture. Add pack 99047 to test-fixture packs.json.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ItemEntry gains Type (client item_type enum, 1=challenge, 2=card-pack
ticket, 3=premium orb, 4=colosseum, 5=orb piece, 6=skin/event ticket,
7=other) and ThumbnailPath. ItemImporter mirrors PaymentItemImporter
shape: find-or-create per item_id, save once, idempotent. Wired into
Bootstrap.Program and SVSimTestFactory.SeedGlobalsAsync. Unblocks
/item_purchase/info (filters card-pack tickets by Type==2) and any
reward grant of UserGoodsType.Item, which previously threw because
the catalog was empty.
466 tests pass (was 461).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stage 9C of the bootstrap-seed-refactor:
- Add 6 seed DTOs for the card-id-keyed load-index tables (SpotCard,
ReprintedCard, UnlimitedRestriction, LoadingExclusionCard, MaintenanceCard,
FeatureMaintenance).
- Add CardListsImporter: idempotent upsert of the 6 tables, sharing one
Cards FK set for orphan-warning. FeatureMaintenances clear-and-rewrites
(synthetic ordinal Id; no natural key).
- Add RotationFlagUpdater: reads RotationConfig.RotationCardSetIds from the
GameConfigs section (populated by RotationConfigImporter) and flips
CardSet.IsInRotation to match.
- Add RotationConfig.RotationCardSetIds list property + wire it through
RotationConfigImporter. No migration needed (sections are JSON blobs).
- RotationConfigImporter: use legacy local-kind DateTime parse for schedule
windows so the JSON round-trip stays byte-equivalent to GlobalsImporter.
- Strip GlobalsImporter down to a no-op stub (Task 10 will delete it).
- Wire all 9 new importers into Program.cs and SVSimTestFactory.SeedGlobalsAsync,
in the order RotationConfigImporter -> ... -> CardListsImporter -> RotationFlagUpdater.
- Delete prod-captures/load-index-2026-05-23.json.
- Add CardListsImporterTests covering each sub-table, idempotency,
empty-seed handling, orphan-warning, and the clear-and-rewrite path.
Tests: 391 passing (382 baseline + 9 new).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extracts /deck/info's default_deck_list into seeds/default-decks.json
via the new extract-default-decks.ps1 PowerShell script and imports
through DefaultDeckImporter. The importer carries the same orphan-
card-id warning the old GlobalsImporter path emitted; production cards
yield 0 orphans. WarnOrphans stays inside GlobalsImporter for now —
SpotCards/ReprintedCards/UnlimitedRestrictions/LoadingExclusionCards
still use it until Task 9.
Part of the bootstrap seed refactor (Task 6).
Replaces GlobalsImporter's ImportPuzzleGroups/Puzzles/Missions methods (plus the
DeriveTargetPuzzleGroupId regex helper) with a dedicated PuzzleImporter that
reads three flat seed JSONs (puzzle-groups, puzzles, puzzle-missions) produced
by the Python extractor. Groups run before puzzles to satisfy the FK; missions
upsert by sequential id. Wired into Program.cs and SVSimTestFactory after
PaymentItemImporter so existing GlobalsImporterPuzzleTests continue to pass
unchanged via SeedGlobalsAsync. The original prod-capture JSONs are deleted now
that the seeds are authoritative.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lifts ImportPaymentItems out of GlobalsImporter into a dedicated
PaymentItemImporter driven by Data/seeds/payment-items.json. Wired
into Program.cs and SVSimTestFactory.SeedGlobalsAsync after
PracticeOpponentImporter. Drops the prod-capture file in favor of
the extractor pipeline.
Canonical 4-test suite (basic, idempotent, leave-untouched, skip-zero)
keeps the dict-in-sync upsert pattern Task 2 established.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move /practice/info handling out of GlobalsImporter into a dedicated
PracticeOpponentImporter that reads a normalized JSON seed file
generated by data_dumps/extract/extract-practice-opponents.ps1.