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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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.
- 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.
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>
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.
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.
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.
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>
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.
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>
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>
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>
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>
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>
- 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>
- 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>
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.
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>
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.
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>
- 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>
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>
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>