Commit Graph

60 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 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 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 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 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 b5134ef86d fix: season list re-reads via provider dependency; ignore pid files
seasonsProvider now watches seasonRefreshProvider instead of relying on
a HomeScreen ref.listen, making the refresh ordering-independent. Removes
the redundant listener from HomeScreen. Appends *.pid to .gitignore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 22:49:24 +09:00
airkjw 9763968db9 fix: hide spent rescue to prevent StateError crash; log per-attempt starts
Expose engine.rescueUsed getter and surface it through GameViewState so
the result overlay can omit the watch-ad FilledButton after a rescue has
been consumed, preventing a second tap from calling useContinue /
addExtraMoves and hitting their StateError guard. Give-up is promoted to
FilledButton when rescue is unavailable for clear affordance.

Also emit stageStart / endlessStart analytics in restart() so every
attempt (not just the first) is bracketed by a matching start event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:41:59 +09:00
airkjw 074a21ea2b feat: analytics abstraction with debug backend and game event wiring
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:35:31 +09:00
airkjw 6d2d97bfcc fix: guard journey map against an empty season list
Prevents StateError when data builder is called with empty list
by displaying loading indicator instead of passing empty list to
activeSeason(), matching title screen behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:31:22 +09:00
airkjw 4fa5564975 feat: session content sync trigger and newest-season selection
Add seasonRefreshProvider (once-per-session FutureProvider) and activeSeason()
helper; HomeScreen listens and invalidates seasonsProvider when new packs arrive;
season_map_screen and season_title_screen switch from list.first to activeSeason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:28:39 +09:00
airkjw c59454aa5f fix: center splash logo and wordmark (stack was sized to children)
Wrap AnimatedBuilder in SizedBox.expand so the Stack fills the full
Scaffold body; alignment: Alignment.center now centers within the whole
screen instead of within the wordmark-sized intrinsic box.

Adds a regression widget test (test/ui/splash_screen_test.dart) that
asserts the wordmark dx is within 1px of screen-center at 1500ms into
the animation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 07:37:54 +09:00
airkjw bf7720ebd3 fix: clear season flow when starting Classic so tutorial/theme can't leak
- Add SeasonFlowNotifier.clear() to null out the flow state
- Call clear() in HomeScreen's Classic button before startStage()
- Broaden tutorial-end guard to next.phase != GamePhase.playing (covers stuck)
- Add regression test: clear() resets flow to null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 07:33:41 +09:00
airkjw 26adf98d73 fix: guard home buttons against double-tap double-push 2026-06-12 07:22:18 +09:00
airkjw 94e62d3e41 feat: redesigned home with adventure/classic entries and endless best
Add adventure/classic l10n keys; rewrite HomeScreen with SeasonBackground,
2×2 glossy logo mark, Adventure→SeasonMap + Classic→endless GameScreen buttons,
and conditional best-score caption. Update widget_test assertions accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 07:18:42 +09:00
airkjw 5a84a47cd4 fix: keep HUD balance in endless via invisible moves chip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 07:13:47 +09:00
airkjw fea8336391 feat: endless mode UI - game over card, best score, HUD 2026-06-12 07:06:30 +09:00
airkjw 2b44dcd812 fix: journey map season-complete styling, scroll callback guard, palette constant
- Guard addPostFrameCallback with !_autoScrolled so it only fires once per
  widget lifetime instead of on every LayoutBuilder rebuild.
- Derive seasonComplete flag; pass it into _node so the last stage uses
  gold "done" styling (not "current/next") when the season is fully 3-starred.
- Extract const Color(0xFF232B4A) to GamePalette.lockedNode.
- Remove warnIfMissed: false from tap call in season_map_screen_test; ensureVisible
  already guarantees hit-testing succeeds (confirmed: test still passes cleanly).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 23:30:22 +09:00
airkjw 78eb5c0639 feat: serpentine journey map with auto-scroll and glowing current node
Replaces the plain GridView with a Candy-Crush-style journey map:
dotted serpentine path, circular nodes (gold=done, glowing=current,
dark+lock=locked), glass header, auto-scroll to current stage.
Updates season_map_screen_test to use Key('stage_node_$i') finders.
2026-06-11 23:17:41 +09:00
airkjw 96304cc8a7 feat: serpentine map layout function 2026-06-11 23:12:29 +09:00
airkjw ee364cc2e2 fix: dismiss tutorial when the stage ends
A finished stage ends the tutorial; otherwise the overlay would sit
on top of the result card and leak into the next stage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 23:11:13 +09:00
airkjw 963d0d5dd6 feat: first-play interactive tutorial overlay
Add TutorialOverlay widget (dim veil, message bubble, animated hand on
dragPiece step, skip button) and wire it into game_screen: start on
flow.index==0 when tutorialDone is false, forward onPlaced/onLineCleared
events unconditionally from fxTick handler, and compute hand-path
coordinates from board/tray RenderBox geometry.
2026-06-11 22:58:14 +09:00
airkjw f97b4faad7 fix: bail to home when season list is empty on title card 2026-06-11 22:48:17 +09:00
airkjw 9fe1910d12 feat: season title card on cold start
Shows "SEASON 1 / First Bloom / N stages" over the season background for
~1.6s between splash and home; tap anywhere skips. Bails to home on
content-load error. Adds seasonLabel/seasonStages l10n keys (en + ko).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:43:20 +09:00
airkjw 3f34358137 refactor: splash nextScreen as constructor param instead of mutable static
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:39:04 +09:00
airkjw 189ab469af feat: logo-assembly splash screen and native launch colors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:30:04 +09:00
airkjw 944c5733c9 fix: defensive keys and overflow guard in result overlay
- Add ValueKey(i) to each TweenAnimationBuilder in the star animation loop
  to prevent Flutter from reusing widget state during rebuild cycles
- Add maxLines: 2 and overflow: TextOverflow.ellipsis to the almostThere
  Text widget to prevent overflow in the loss-state progress message

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:27:14 +09:00
airkjw 677a09f8cb feat: staggered star reveal and near-miss progress ring
Stars on win now appear sequentially with elastic-bounce via
TweenAnimationBuilder (400/650/900 ms). Lost overlay shows a
CircularProgressIndicator ring with "87% complete!" (l10n:
almostThere) when objectiveProgress > 0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:20:29 +09:00
airkjw f1b8052f77 fix: re-anchor effects clock when ticker drains (stale-clock freeze)
After Ticker.stop(), elapsed resets to zero on the next start(). _now was
left frozen at the old elapsed, so effects added after a drain captured a
stale start time and their progress() clamped to 0 forever — ticker never
stopped, second batch broken. Fix: reset _now = Duration.zero on drain.
Adds @visibleForTesting getters and a regression test that catches the
stale value directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:16:51 +09:00