Commit Graph

195 Commits

Author SHA1 Message Date
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
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
c7fb56f95f test(pack): cover additive accrual on existing gacha-point balance
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 23:27:56 -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
ef1af8259e feat(pack): gacha-point endpoint DTOs 2026-05-28 22:56:33 -04:00
gamer147
96f1d73e35 docs: fix memory-reference name in gacha-point model docstrings 2026-05-28 22:54:46 -04:00
gamer147
21adc68e28 feat(db): add gacha-point balance + received tables 2026-05-28 22:50:22 -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
22c01ed11a fix(viewer): fresh signups start with empty DisplayName
Verified against Wizard.Title/UserNameInput.cs:30 in the 2026-05-23
decompile:

    IsFinished = !string.IsNullOrEmpty(PlayerStaticData.UserName);

Any non-empty seeded value — including the prior " - " placeholder
this method was passing — sets IsFinished=true on the first frame and
silently skips both the input dialog and the /tutorial/update_action #1
+ /account/update_name calls that travel with it. The in-source comment
described the opposite behavior; empty string is what actually triggers
the dialog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:19:03 -04:00
gamer147
b18bb9502a fix(pack): /pack/info reads ItemId via shadow FK, not nav property
PackController.Info's ownedItemsByItemId projection used `i.Item.Id` to
key the dict — EF translates that to the FK column today, but any future
model change that breaks the nav→column mapping would fall back to client
eval and collapse every key to 0 (the default Item constructor's Id),
silently hiding every tutorial pack via item_number=0. EF.Property<int>
reads the shadow FK directly and is robust to nav changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:15:40 -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
91412ff821 fix(account): /account/update_name validates name input
Reject empty / whitespace / explicit-null / over-cap names with 400
instead of NREing on null assignment or storing arbitrarily-long
strings the DB column has no cap on. 24-char limit is a conservative
backstop against direct API abuse; the client UI enforces its own
keyboard limit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:11:27 -04:00
gamer147
d13082a8ca fix(gift): receive response is idempotent and echoes persisted state
Three coupled correctness fixes to /tutorial/gift_receive's response:

- received_ids / total_receive_count_list / reward_list are now built
  from `toClaim` (the gifts THIS call granted), not from `requestedIds`.
  Echoing the client's request meant idempotent re-claims re-fired the
  "+N received" popup and direct-assigned the same post-state totals
  again, breaking the documented idempotency contract.
- is_unreceived_present is now `unclaimedPresents.Count > 0`. The
  hardcoded false hid the inbox badge after partial claims even when
  present_list still carried unclaimed gifts.
- tutorial_step echoes the persisted (max-preserved) state instead of
  a hardcoded 41. A replay against a state>=41 viewer used to surface
  41 on the wire and regress the client's tutorial state machine.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:10:06 -04:00
gamer147
86759125a9 fix(gift): tutorial gift_receive ThenIncludes OwnedItemEntry.Item
Same project_ef_nav_include_pitfall as 27ebb51's tutorial pack_open fix
but in the gift path: without .ThenInclude(i => i.Item), the existing
OwnedItemEntry's Item nav defaults to a new ItemEntry() (Id=0), so
RewardGrantService.ApplyAsync's `FirstOrDefault(i => i.Item.Id == detailId)`
misses pre-existing rows. It falls through to add a new entry, and the
(ViewerId, ItemId) unique index added 2026-05-25 throws on SaveChanges →
500 to the client, no tutorial advancement, no currency grant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:08:19 -04:00
gamer147
82d9668c9b fix(pack): /tutorial/pack_open restricted to starter pack + pre-END viewer
The tutorial alias bypassed the currency / type_detail / open-count guards
unconditionally. Combined with the unconditional TutorialState=100 write, any
authenticated viewer could send /tutorial/pack_open with any parent_gacha_id
to draw a pack for free and clobber their state down to 100.

Two gates: parent_gacha_id MUST be 99047 (the legendary starter), and the
viewer's TutorialState MUST be below 100. The state write is also max-preserved
as a belt-and-braces backstop. Mirrors the 31→41 guard in GiftController.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:31:23 -04:00
gamer147
6fd8705990 fix(tutorial): /tutorial/update preserves max TutorialState
The endpoint used to write the client-supplied step verbatim, so a stale or
replayed request with tutorial_step=0 against any later-stage viewer would
regress the persisted state to 0. NextSceneSwitcher routes step==0 to
AreaSelect section 0, which has no chapter data — the client LINQ-Single()
crashes on next /load/index, bricking the viewer. Math.Max-preserve matches
the 31→41 pattern in GiftController.TutorialGiftReceive.

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