Commit Graph

101 Commits

Author SHA1 Message Date
airkjw 410182cf7d feat(ui): floating pulse hint for booster targeting
Replaces the plain bottom SnackBar with a BoosterHint pill that floats in
the empty space above the board: season-accent coloured, a breathing glow
that pulses only while armed (idle otherwise — no wasted ticker), slides/
fades in, shows the booster icon + prompt, and cancels on tap. 3 widget
tests; full suite 234 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 20:53:52 +09:00
airkjw 42deeaf242 fix(boosters): count hammer/line-bomb clears toward objectives and win
Owner playtesting found that hammering a gem (or line-bombing a gem line)
removed it visually but did not satisfy the clear-gems objective and never
completed the stage — it felt broken. Per owner decision, booster clears
now count: a hammered gem emits gems:1, a line-bomb emits lines:1 + the
line's gem count, both folded through the objectives. After any booster the
engine resolves the phase (completed objective -> won, else re-check stuck),
which was the direct cause of the stage not finishing. Score and the move
counter remain untouched. Reverses the earlier no-objective-credit rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 20:15:00 +09:00
airkjw fa2784519b fix(boosters): address final-review findings
- daily claim: record the claim before granting boosters, so a crash
  mid-claim forfeits at most one reward instead of allowing a re-claim
  (booster farming) on next launch.
- game screen: disarm the booster target synchronously before awaiting,
  so a rapid second board tap can't double-fire a use or stack a dialog.
- new players: seed one of each booster once (idempotent persisted flag),
  fulfilling the spec's starting inventory. Wired in main().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:36:24 +09:00
airkjw 412cc08167 fix(l10n): localize line-bomb row/column chooser labels
The line-bomb axis chooser hardcoded Korean 가로/세로; route them through
new boosterLineRow/boosterLineCol keys (EN: Row/Column).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:27:41 +09:00
airkjw 1a028b9852 feat(ui): 7-day daily-reward popup on home
Presentational DailyRewardSheet (7 cells, today highlighted, reward icons
from the calendar table, claim + watch-ad-2x buttons). HomeScreen becomes a
ConsumerStatefulWidget that shows it once per mount via a post-frame guard;
the 2x path grants the doubled reward only if the rewarded ad was earned,
else the base reward. Guards the throwing saveRepositoryProvider default so
a repo-less mount is a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:26:43 +09:00
airkjw b8bfa00196 feat(ui): rewarded-ad grant for an empty booster
Tapping an empty booster opens the get-one dialog; confirming watches a
rewarded ad and, on earn, grants +1 of that booster and logs booster_granted
(source: ad). Covered by a widget test using an uninitialized AdService whose
showRewarded() resolves true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:41:27 +09:00
airkjw 1ba30028b5 feat(ui): booster bar targeting in the game screen
Mount BoosterBar below the tray (only while playing), guarded so legacy
GameScreen tests without a SaveRepository keep passing. Tapping a booster
arms targeting: shuffle applies immediately; hammer/line-bomb arm a board
tap (hammer clears a cell, line-bomb opens a row/column chooser). An empty
booster opens a get-one dialog (ad grant lands in Task 15).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:40:13 +09:00
airkjw a04bb3b847 feat(ui): presentational booster bar 2026-06-18 12:28:25 +09:00
airkjw 0517fabdbb feat(l10n): booster + daily-reward strings (EN/KO)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 12:26:31 +09:00
airkjw d0a2be15ba feat(analytics): booster + daily-reward events
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 12:24:02 +09:00
airkjw fa4247cd9b feat(state): DailyRewardNotifier with injectable clock 2026-06-18 12:21:02 +09:00
airkjw ba4d4a662b feat(state): coordinate booster use with inventory in the session 2026-06-18 12:17:30 +09:00
airkjw 638a177fbb feat(state): BoosterInventoryNotifier 2026-06-18 12:14:34 +09:00
airkjw c185bd0886 feat(daily): pure 7-day login-calendar logic + reward table 2026-06-18 12:11:15 +09:00
airkjw 544a2b8be4 feat(save): persist daily-reward claim state 2026-06-18 12:09:06 +09:00
airkjw 221ea8346e feat(save): persist booster inventory 2026-06-18 12:08:17 +09:00
airkjw 6592b44387 feat(model): BoosterType enum 2026-06-18 12:07:19 +09:00
airkjw e7cd079a5d feat(engine): line-bomb booster clears a row or column 2026-06-18 12:05:14 +09:00
airkjw bbf8cf3f08 feat(engine): shuffle booster re-deals the tray 2026-06-18 12:04:00 +09:00
airkjw 5aee503c09 feat(engine): hammer booster removes one cell, no scoring 2026-06-18 12:02:55 +09:00
airkjw 02021b540e feat(crashlytics): report release crashes to Firebase Crashlytics
Routes Flutter framework errors (FlutterError.onError) and uncaught
async/platform errors (PlatformDispatcher.onError) to Crashlytics, but
only in release builds — debug keeps its red error screens and console
traces, and collection is disabled via setCrashlyticsCollectionEnabled
(kReleaseMode) so development crashes never reach the dashboard. Adds
the Crashlytics Gradle plugin alongside the existing google-services
FlutterFire config. iOS dSYM upload run script still to be added in
Xcode (symbolication only; crash capture works without it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:17:27 +09:00
airkjw cec4c3e427 feat(review): request a store review after a 3-star win, once
Adds an in-app review prompt gated by ReviewPromptPolicy: only after a
3-star stage win, once the player has cleared >=5 stages, at most once
ever (persisted reviewRequested flag). ReviewService swallows all
failures and only burns the one-shot when the store actually shows the
sheet, so an unavailable store retries on a later win. StoreReviewer
wraps in_app_review behind a Reviewer seam so unit tests skip platform
channels. 13 new tests; full suite 194 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:13:55 +09:00
airkjw 8b5bbd9531 feat: ship ad-supported only — gate Remove Ads IAP behind kIapEnabled=false
The developer is an individual without a Korean business registration, so
the App Store / Play paid-apps (merchant) agreements can't be completed.
Hide the Remove Ads + Restore tiles and skip IAP init; ads always show.
AdMob revenue is independent of those agreements. Reversible: flip
kIapEnabled to re-enable once a merchant agreement exists. Bump to build 2;
drop the now-unused IAP review-screenshot generator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:55:48 +09:00
airkjw c7c558cb96 release: drop test-only build tag from settings footer
The '(build 4)' marker was a device build-delivery debugging aid, now
resolved. Public release shows a clean version string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:47:33 +09:00
airkjw 7c7c7afad0 content: regenerate Season 1 with a gentle onboarding difficulty curve
Generator: raise the early bot-win-rate floor to ~0.97 (near-guaranteed first
stages), widen the early move-buffer margin, and delay score-chase/line-sprint
objectives until stage 9/13 so the opening stretch is pure clear-a-gem. Result:
stage 1 now 16 moves (was 11), stages 1-9 all simple gems at 90-99% bot win.
Preserves theme.bgm=season_001; manifest SHA regenerated. Settings tag -> build 4.
2026-06-14 11:02:29 +09:00
airkjw 1682578501 feat: start at Season 1, add menu/season_001 BGM tracks, settings build tag
- activeSeason now returns the first season (Season 1 'First Bloom') so a new
  player starts at spring instead of the newest season.
- Bundle the owner-picked CC0 tracks menu.mp3 + season_001.mp3 (BGM now audible).
- Settings footer shows 'v1.0.0 (build 3)' so test builds are identifiable.
180 tests green, analyze clean.
2026-06-14 10:32:12 +09:00
airkjw 8947221b27 feat(audio): looping per-season background music system
MusicService (looping audioplayers player, independent of SFX) driven by the
active season theme's new 'bgm' key; switches track on season change, pauses on
app background, all failures swallowed. Separate Music on/off toggle in Settings
(persisted, independent of SFX). Season packs carry bgm keys (menu/season_001/
season_002), manifest regenerated. Assets slot assets/audio/bgm/ ready — drop in
menu.mp3/season_001.mp3/season_002.mp3 (CC0) and it plays; silent until then.
180 tests green, analyze clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 09:31:10 +09:00
airkjw 2310aabdb9 fix(ui): move game-screen close button to its own top row (was overlapping HUD)
On device the floating top-left X (48px IconButton) overlapped the HUD moves
chip. Move it into the column flow above the HUD, left-aligned, so it never
collides. Found via on-device test build.
2026-06-14 09:02:01 +09:00
airkjw 734c8a4cf7 feat(ads): wire real AdMob ids (Phase 5 finalize)
Replaces the TODO_REAL_* ad-unit placeholders with the owner's real AdMob
console ids (publisher pub-5605900229781491) and the native AdMob app ids
(iOS ~8397095848, Android ~8257495040) in place of the Google sample ids.
Debug builds still serve Google test ads via kDebugMode; release builds now
use the real units.
2026-06-13 19:05:10 +09:00
airkjw 23f90d5b89 style: drop unnecessary dart:ui import in app_icon_painter 2026-06-13 18:25:13 +09:00
airkjw b0839aba2a fix(store): render feature-graphic wordmark with real font (Titan One, OFL)
The flutter_test default font draws every glyph as a box, so the wordmark was
unreadable. Load the OFL Titan One TTF via FontLoader in the generator and
render 'Block Seasons' + tagline with it; fit the text within the 1024 width.
Font used only by the asset generator, not bundled in the app.
2026-06-13 18:20:11 +09:00
airkjw 7fe2bc2776 feat(store): Play feature graphic (1024x500) generator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 18:15:11 +09:00
airkjw 536807e7c8 feat(juice): button press feedback + fade screen transitions
Adds PressableScale (0.94 squish on tap-down) around the Adventure
FilledButton, Classic OutlinedButton, and each season-map stage node.
Replaces all in-app MaterialPageRoute pushes with a gentle fade+scale
PageRouteBuilder (320ms in, 240ms out) via a new fadeRoute helper.
2026-06-13 18:12:07 +09:00
airkjw 099ced377d feat(brand): app icon painter + generated 1024px icon PNGs
Introduces AppIconMark (navy gradient field + 2×2 glossy blocks reusing
paintGlossyTile) and a testWidgets generator that writes icon.png,
icon_background.png, and icon_foreground.png to assets/icon/ for the
upcoming flutter_launcher_icons task.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 18:06:18 +09:00
airkjw 498fb6af83 feat(settings): sound & vibration toggle; themed settings screen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 18:01:23 +09:00
airkjw 3ca038ec65 feat(settings): soundEnabled provider gates SFX and haptics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 17:59:08 +09:00
airkjw 93397988a2 feat(settings): persist soundEnabled flag (additive, default true)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 17:56:22 +09:00
airkjw 7bea9c1456 fix(iap): eagerly start IAP service at launch (final review I-1)
iapServiceProvider was lazy and only read by Settings, so the purchase stream
attached only after opening Settings — deferred/interrupted/restored
transactions wouldn't be delivered or completed until then. Read it in the
post-frame callback so the stream is live for the whole session.
2026-06-13 14:29:20 +09:00
airkjw 640b23804f fix(ads): banner retries load when SDK becomes ready
On a cold start the consent flow is still running when home first builds, so
createBanner returns null and the slot stayed empty until a rebuild. AdService
now exposes an isReady ValueNotifier; BannerAdSlot listens and retries its load
once MobileAds finishes initializing. Verified: analyze clean, 169 tests green.
2026-06-13 14:23:45 +09:00
airkjw 70f87ab8f2 feat(iap): settings screen with remove-ads purchase and restore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 14:17:28 +09:00
airkjw 40c2204d7b feat(ads): home/map banner slot (hidden when removed or unloaded)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 14:13:29 +09:00
airkjw 1ec59ba80d fix(ads): reset ad round in startStage so nextStage advance counts (review fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 14:09:02 +09:00
airkjw 297449ccce feat(ads): stage-end interstitial gated by policy; restart resets round 2026-06-13 14:03:52 +09:00
airkjw 3943653a23 feat(ads): rewarded ad gates the continue/extra-moves rescue 2026-06-13 14:02:31 +09:00
airkjw 662ee55e1d feat(ads): run consent flow (UMP->ATT->init) after the first frame
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:59:22 +09:00
airkjw 539afd1dad feat(ads): ref-constructed ad/consent/iap providers, single ownership source 2026-06-13 13:57:30 +09:00
airkjw 6d2ffebb92 feat(iap): remove_ads purchase/restore service + adsRemoved notifier 2026-06-13 13:55:44 +09:00
airkjw e43fda8551 feat(ads): ConsentService enforcing UMP -> ATT -> SDK init order 2026-06-13 13:53:10 +09:00
airkjw 4744aa167a feat(ads): AdService for interstitial/rewarded/banner with policy gating 2026-06-13 13:49:18 +09:00
airkjw eb258c7324 feat(ads): ad id config with test/real switch via dart-define 2026-06-13 13:46:44 +09:00