Commit Graph

967 Commits

Author SHA1 Message Date
gamer147
6fb73c7a09 feat(config): LoginBonus section with 15-day shipped normal cycle
Rewards copied verbatim from traffic_prod_tutorial.ndjson capture.
2026-06-13 15:50:42 -04:00
gamer147
107ee106a3 feat(viewer): add LastLoginBonusClaimedAt + LoginBonusStreak columns
Drops the unused daily_login_bonuses table in the same migration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 15:42:40 -04:00
gamer147
f707fb2ffb refactor: drop unused DailyLoginBonuses table + importer
The captured shape ({"1":[], "3":[], "4":[]}) is the empty-period
wire echo; the client parser (LoadDetail.cs:553) only reads normal/total/
campaign. Table, entity, seed, and importer were never wired to a code path.

Replaced by config-section catalog + service in the follow-up commits.
2026-06-13 15:32:33 -04:00
gamer147
ba7040580f fix(arena-colosseum): tips + colosseum_id wiring + round-aware time derivation
Three fixups the Phase 1-3 work missed (would bite the first time an admin
flipped on a real event):

* IsDisplayTips + TipsId added to ColosseumSeasonConfig. Drives
  Wizard/ColosseumEntryInfoTask.cs:129 (NeedsFirstTips = is_display_tips == 1).
  The wire DTO already had the properties; the season config didn't carry
  them, so admins had no way to gate the first-time tutorial tip popup.
* colosseum_id is now emitted from SeasonId. Wire field was always-absent
  before (DTO had the property but builder never set it); client fell back
  to 0 and couldn't disambiguate cups.
* ColosseumLobbyInfoBuilder.Build now takes ColosseumRoundsConfig + nowUtc
  and derives the wire is_round_period / now_round / next_round / start_time
  / end_time from the rounds schedule against the current clock. Previous
  hardcode (is_round_period: true, now_round: 1, event-window dates) would
  have shown "Round 1 ends 2026-07-31" instead of the actual round's end.
  Per the client parser at ColosseumEntryInfoTask.cs:111-126:
    - inside a round → emit that round's window + is_round_period=true
    - between rounds → emit next round's start as start_time + next_round
    - pre-event → next_round=1, start_time=event start
    - post-event → anchor to last round so the client doesn't NRE
    - no rounds configured → fall back to event-level window

Why these were missed: I implemented ColosseumSeasonConfig from the plan's
explicit field list rather than cross-checking against the captured
colosseum.json wire shape; I added ColosseumId to the wire DTO without
wiring the builder side; and no test exercised a mid-event scenario with
realistic dates — Phase 2's E2E hand-edits run.RoundId via
PromoteRunToRoundAsync instead of letting the controller derive it.

* 10 new builder unit tests cover off-season / mid-round / between-rounds /
  pre-event / post-event / empty-rounds-fallback + the new field surfacing.
  Full suite: 1355/1355 (was 1345 + 10).
2026-06-13 13:44:10 -04:00
gamer147
43a3665775 refactor(arena-colosseum): dedup colosseum_info source-of-truth
/mypage/index data.colosseum_info and /arena_colosseum/{top,get_fee_info}
now both project through ColosseumLobbyInfoBuilder.Build(ColosseumSeasonConfig).
One source of truth for the home-tab gating and the lobby reads — admins
flip the season on by writing to GameConfigs (per the runbook), no second
table to keep in sync.

* New ColosseumLobbyInfoBuilder — static projection from ColosseumSeasonConfig
  to the wire ColosseumLobbyInfo. Replaces ArenaColosseumController's private
  BuildColosseumInfo and MyPageController's BuildColosseumInfo(ColosseumConfig).
* MyPageController reads ColosseumSeasonConfig via IGameConfigService; the
  IGlobalsRepository.GetCurrentColosseum() call goes away.
* MyPageIndexResponse.ColosseumInfo retyped to the new ColosseumLobbyInfo.
* ColosseumSalesPeriodInfo relocated into the ArenaColosseum namespace
  alongside the rest of the family DTOs.
* Drops: Colosseums DB table (migration DropColosseumsTable), ColosseumConfig
  entity, the captured-shape Models.Dtos.ColosseumInfo DTO, ColosseumSeed,
  MyPageGlobalsImporter.ImportColosseumAsync + seed file colosseum.json,
  IGlobalsRepository.GetCurrentColosseum + impl, the two related tests.

Net: home-screen Grand Prix tab now hidden by default because the season
ships with IsColosseumPeriod=false. Full suite: 1345/1345 (was 1347 — the
2 dropped tests covered the now-dead surface).
2026-06-13 13:31:03 -04:00
gamer147
114e3da81f Merge feat/arena-colosseum: 3-phase Grand Prix implementation (1/16 → 15/16) 2026-06-13 13:00:58 -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
110867358c feat(arena-colosseum): lobby + constructed entry (phase 1)
Closes 5 of arena-colosseum's 16 spec shapes (1/16 → 6/16). Lobby reads
(/top, /get_fee_info, /event_info) render an empty "no event scheduled"
payload by default; /entry + /register_deck activate via admin-flipped
ColosseumSeason + ColosseumRounds config sections.

* Schema: ViewerArenaColosseumRun standalone table (unique on ViewerId)
  with jsonb run-state columns mirroring ViewerArenaTwoPickRun.
* Config sections: ColosseumSeasonConfig (event-level), ColosseumRoundsConfig
  (the 3-round bracket). Empty defaults — IsColosseumPeriod=false.
* Migration AddArenaColosseumRun (DDL only; seed rows come from the section
  ShippedDefaults via EnsureSeedDataAsync).
* DTOs: ColosseumLobbyInfo (round-level, /top + /get_fee_info), ColosseumEventInfo
  (event-level, /event_info), ColosseumOwnStatus (shared status block),
  ColosseumEntryRef, ColosseumFeeList, ColosseumUserDeck, ColosseumBattleResults,
  ColosseumRoundDetail + ColosseumGroupRow. EventInfoResponse uses explicit
  [JsonPropertyName("1"|"2"|"3")] for the string-keyed rounds shape per spec.
* Controller: ArenaColosseumController replaces the 1-action stub with
  the five lifecycle endpoints. /top emits leader_skin_id even when 0
  (project_wire_null_policy override).
* Tests: 11 controller-level tests + 6 config round-trip tests covering
  empty-season payloads, run-seeded /top round-trip, crystal/rupy entry
  debits, now_round_id mismatch rejection, deck_no_list round-trip + reject.
2026-06-13 12:16:22 -04:00
gamer147
a5fe484775 feat(account): /account/update_birth persists viewer birth date
Strict yyyy-MM-dd parse (matches /load/index round-trip format) — rejects
malformed input. data_headers.servertime is emitted by the standard
envelope, which is what the client reads into BirthDayUpdateServerTime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 11:35:41 -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
b5e13551cd Merge branch 'feat/decomp-only-followups' 2026-06-13 10:07:12 -04:00
gamer147
43dee9d00b feat(card): /card/create_foil_card Seer's Globe conversion (1 orb + 1 base → 1 foil) 2026-06-13 09:58:55 -04:00
gamer147
13ca7d118e feat(config): persist is_foil_preferred + is_prize_preferred (stubs → real) 2026-06-13 09:54:51 -04:00
gamer147
911374106f feat(emblem): /emblem/update_emblem sets viewer.SelectedEmblem (ownership-validated) 2026-06-13 08:16:19 -04:00
gamer147
bc110e2274 feat(emblem): /emblem/favorite accept-only (persistence deferred) 2026-06-13 08:15:29 -04:00
gamer147
921297227e feat(emblem): /emblem/emblem_list returns owned ids 2026-06-13 08:14:36 -04:00
gamer147
c5c61b6e9d feat(degree): /degree/update_degree sets viewer.SelectedDegree (ownership-validated) 2026-06-13 08:13:49 -04:00
gamer147
356e58ba25 feat(degree): /degree/favorite accept-only (persistence deferred) 2026-06-13 08:12:02 -04:00
gamer147
68741b2980 feat(degree): /degree/degree_list returns owned ids 2026-06-13 08:11:13 -04:00
gamer147
025c2a8c56 feat(mission): /mission/receive_reward stub (refresh-only, no decomp)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 23:33:50 -04:00
gamer147
e54e63a762 feat(mission): /mission/buy_additional_right stub (refresh-only, no decomp)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 23:29:55 -04:00
gamer147
30b8af2d16 feat(pack): /pack/get_leader_skin_owned_status (empty per-class buckets) 2026-06-12 23:26:29 -04:00
gamer147
c870b95be9 feat(sleeve): /sleeve/favorite accept-only (persistence deferred) 2026-06-12 23:22:25 -04:00
gamer147
f879c05dca feat(sleeve): /sleeve/sleeve_list returns owned ids 2026-06-12 23:17:10 -04:00
gamer147
e2f5786b60 feat(mypage): /mypage/get_special_crystal_info (empty — no offers) 2026-06-12 23:12:49 -04:00
gamer147
43e4c06a2c feat(mypage): /mypage/finish_battle (returns check=0 — no in-flight state yet) 2026-06-12 23:08:42 -04:00
gamer147
b4a87792dc feat(account): /account/update_region_code (accept-only) 2026-06-12 23:01:53 -04:00
gamer147
56ef994275 style(profile): drop file-path header comments from quick-wins task 1 2026-06-12 22:59:35 -04:00
gamer147
398764a4cc feat(profile): /profile/update_official_mark_display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 22:54:52 -04:00
gamer147
37cb8b5eeb test(free-battle): literal-prod-JSON parity for /do_matching 2026-06-12 21:30:19 -04:00
gamer147
55ec279f73 test(free-battle): force_finish no-op returns empty object 2026-06-12 21:29:52 -04:00
gamer147
31ecda95f7 test(free-battle): consistency battle_result echoes on rotation URL 2026-06-12 21:29:26 -04:00
gamer147
523ac605a1 test(free-battle): /finish echoes battle_result with no rank fields 2026-06-12 21:29:02 -04:00
gamer147
487a86fbbb test(free-battle): 3001 when viewer has no deck for format 2026-06-12 21:28:35 -04:00
gamer147
db298a0652 test(free-battle): two-viewer PvP pairing flow 2026-06-12 21:28:00 -04:00
gamer147
4a09b03820 test(free-battle): DoMatching first poll returns 3002 2026-06-12 21:27:33 -04:00
gamer147
3e58fbb650 feat(free-battle): FreeBattleController + routing smoke tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 21:24:02 -04:00
gamer147
84760490e7 feat(free-battle): register PvpOnly ModePolicy for rotation+unlimited 2026-06-12 21:17:24 -04:00
gamer147
1054c86038 feat(free-battle): add FreeBattleFinishResponseDto 2026-06-12 21:17:04 -04:00
gamer147
36eb25da44 feat(free-battle): add FreeBattleFinishRequestDto 2026-06-12 21:16:47 -04:00
gamer147
1a55561c5d feat(free-battle): add DoMatchingResponseDto 2026-06-12 21:16:20 -04:00
gamer147
fe9ec1fc68 feat(free-battle): add DoMatchingRequestDto 2026-06-12 21:15:59 -04:00
gamer147
d0d6a6916c fix(card-master): wire EnableServing kill switch in controller + add to config-section test 2026-06-12 12:52:06 -04:00
gamer147
b4a279ef0c feat(load-index): emit data.card_master_hash on mismatch (tier-1 freshness gate) 2026-06-12 12:27:40 -04:00
gamer147
92a21e7a7a test(routing): cover /immutable_data/card_master routing 2026-06-12 12:20:46 -04:00
gamer147
6eb4547866 fix(card-master): 500 not 503 when blob missing; explicit test-project copy link 2026-06-12 12:19:37 -04:00
gamer147
d74bae9ba0 feat(card-master): implement POST /immutable_data/card_master serving captured blob 2026-06-12 12:11:15 -04:00
gamer147
29f1cdd7d5 fix(card-master): make CardMasterRequest.CardMasterHash non-nullable to match wire spec 2026-06-12 12:09:13 -04:00
gamer147
dc39ed4146 feat(card-master): add request/response DTOs for /immutable_data/card_master 2026-06-12 12:05:14 -04:00