roy_casual_kit 0.6.3
roy_casual_kit: ^0.6.3 copied to clipboard
A casual/idle-game SDK for Flutter (GetX + Flame): storage, i18n, audio, haptics, reminders, economy/progression, live-ops tooling, and a candy-styled widget kit.
0.6.3 #
- Added
WorkloadPerformanceBudgetincore/utils/performance_budget.dartto evaluate integration test frame timings against configurable p95, slow-frame percentage, and minimum frame thresholds with actionable failure diagnostics, plusextractMetricsfor baseline integration. - Added localized tooltips and semantics to
GameDemoScreen's icon-only pause and info FloatingActionButtons (game_demo_pause_tooltipandgame_demo_info_tooltipin bothenandvilocales). - Hardened
MonetizationScreendemo to verifyEconomyWallet.earn()results for consumables and rewarded ads; on storage or persistence failure, error haptics and status are shown without granting phantom consumable ledger items or crediting unearned currency. - Verified: 22 unit tests in
performance_budget_test.dart(including 5 workload evaluator tests), 13 tests inmonetization_screen_test.dart, key parity inapp_translations_test.dart, and 21 tests ingame_demo_screen_test.dartpass. - Verified on physical TECNO KJ7 (Android 14): integration smoke tests for wallet failure consistency and accessible FAB tooltips pass; benchmark runs for accessibility preview panel (202 frames, p95=21.45ms, slow-frame=13.37%) and offline earnings (3227 frames, p95=12.12ms, slow-frame=2.32%) satisfy performance budgets.
- Verified CI: analyzers report 0 issues on package and example; public API compatibility gate is additive; five quality gates pass; GitHub Actions CI Linux (run 40102532740) passes all three jobs. Clean-tree pub publish dry-run passes with 0 warnings.
0.6.2 #
- Use opaque row-index/variant-index keys in
ExperimentVariantAssignmentCard; experiment identifiers remain in callback data and no longer appear in widget keys. - Added
EventSchemaAuditCard, a read-only view of caller-mapped aggregate schema-validation counts (accepted, rejected, redacted). It accepts no event names, parameter names, values or raw violations, and states that schema validation does not confirm analytics delivery. - Verified Wave 33: 7 focused widget tests, root 3,484/example 316 non-golden suites, analyzers, API compatibility and five quality gates pass; 21 Linux + Flutter 3.35.1 golden baselines imported from workflow 40065228127; CI 40065468318 passes all three jobs. TECNO KJ7 smoke passes the aggregate-summary UI test; not evidence of real analytics delivery.
- Verified: root 3,477 and example 315 non-golden tests, analyzers, API compatibility, five quality gates, and a TECNO KJ7 profile-mode override integration test pass.
0.6.1 #
- Fixed
ExperimentVariantAssignmentCardto hide and reject debug variant overrides in release builds, matchingExperimentBucketingService.debugSetVariantOverride. - Restricted
OnboardingFlowQueueCardmark-seen action to caller-designatednextEligibleFlow; loading indicator now applies only to that flow. - Verified: 21 focused widget tests, a profile-mode override integration test on a TECNO KJ7, and CI 39969956180 (all three jobs). A throwaway release-mode probe on the same device rendered both a control and an overridden row with the callback supplied and showed no override or clear button; that probe is not kept in the repo.
0.6.0 #
- Widget kit expansion, additive only: the public API check reports no removed export. The new service-state widgets are presentation-only: pass plain state and callbacks, your code calls the service. Constructors that validate input at runtime are not
const. Failures show generic copy, and a pending call locks the buttons. Some widgets take an explicit status from the caller instead of inferring success from a callback that returns. - The API check (
tool/api_compatibility.dart) now follows localexportbarrels (includingshow/hide, relative paths and cycles) and fails loudly on export forms it cannot resolve. It is still a regex scanner, not the Dart analyzer: class fields and getters are not covered, so a break there is not detected. - Added save-recovery widgets
CloudSaveStatusCard,SaveConflictPicker(keep local / use cloud, each with inline confirmation) andCheckpointResumeCard(confirm before discarding a checkpoint). - Added content-delivery widgets
ContentPackStatusCard,ScenePreloadStatusPanelandContentRevisionHistoryPanel. The panel lists metadata only (opaque revision reference, version, label, time) and never a checksum or content body; restoring needs confirmation, and the caller performs the rollback (for exampleRemoteContentPack.rollbackToChecksum) and refreshes its content. - Added offline-delivery widgets
OutboxQueueStatusCardandOutboxConflictSummaryPanel. Neither shows a queued payload or key; the conflict panel works on caller-approved labels and opaque references, and accepting the remote version states that it does not undo local effects. - Added
PlayerDataRightsPanel(export, plus erase behind inline confirmation, forPlayerDataRightsService),PrestigeReadinessCard(progress toward the threshold; the button is enabled only when ready, and the caller should confirm, for example withPrestigeConfirmationCard.show, before callingPrestigeService.prestige) andCooldownActionButton(disabled while aPersistentCooldownServicecooldown runs). The cooldown gate is UI only: re-check the service inside your tap handler. - Added
ConsentPreferencesPanel(ongoing per-category choices with explicit granted/denied/unknown status; the caller applies the change, for exampleConsentStateService.grant/deny),SyncStatusCard(idle/syncing/synced/failed state with a retry button for achievement or leaderboard sync; the caller maps itsSdkResultto the status) andBatterySaverIndicator(banner while the caller holds performance reduced for low battery; renders nothing when inactive). - Added
RemoteKillSwitchPanel(device-local kill behind inline confirmation, plus clear for an existing local override; it never re-enables a remotely killed feature and shows only caller-sanitized text),PlatformCapabilityMatrix(read-only supported/unsupported list shown by icon and text, with a note that values are platform defaults rather than a live check) andDeepLinkResultCard(generic outcome per status; it never renders the link, its parameters or an exception, so the caller mapsDeepLinkHandleResultto a status). - Added
SessionTimelineInspector(bounded, caller-formatted session-relative history; never accepts raw controller metadata),EconomyCertificateStatusCard(displays verification status and never accepts balances, certificate data or the secret; “valid” means signature checked, not player honesty) andGuardrailStatusPanel(read-only caller-supplied rollout rows, without new read APIs or actions onShadowActivationController). - Added
MemoryWatchdogPanel(bounded caller-fed debug diagnostics; detector only),AnnouncementPreviewCard(shows caller-resolved localized text and invokes a caller preview callback) andRewardedAdResultCard(caller-owned seam state; it never grants a reward, which the caller grants exactly once aftershowRewardedAdreturns true). - Added
AnalyticsQueueStatusCard(caller-supplied queue snapshot: counts and generic status only, never event names or payloads) andSaveRestorePreviewPanel(read-only preview of what a restore would change, with restore behind inline confirmation; the caller performs the restore). - Added
RemoteConfigHealthCard(source and schema-violation count only; no keys or values),ReminderPreferencesPanel(caller-owned preferences; notification delivery may be delayed or skipped), andConnectivityStatusCard(reachability/queue count; queued work drains automatically when online). - Added
RewardPlanPreviewCard(caller-formatted reward lines with explicit ready/stale/executing/applied/failed status; a preview is not a grant, and the caller setsappliedonly from a committed record),ReplayVerificationResultCard(outcome and randomness checks reported separately, with a skipped randomness check shown as skipped, never as a match; it is not proof of fair play) andRewardAuditTrailPanel(sanitized, bounded rows and one retry-all action for unfinished rewards; the caller refreshes the rows afterwards because completion of the retry does not mean every record committed). - Added
OnboardingFlowQueueCard(read-only queue of caller-mapped onboarding flows with seen/next/pending state and an optional mark-seen action; flow ids identify rows in callbacks and are never shown) andExperimentVariantAssignmentCard(caller-mapped experiment assignments; the per-device anonymous id never enters the widget, and the override action is debug-only, so pass the callback only in debug or profile builds). - Added accessibility and player-comfort widgets, retention/progression/social HUD widgets and juice/motion widgets (see
common_widgets.dartand the showcase screen for the full list). - Verified (see each task spec in
doc/task/done/for the exact run): Linux + Flutter 3.35.1 golden baselines per widget wave, full root and example suites, API compatibility, analyzers, five quality gates and CI. Wave 23 device smoke ran on a Samsung S24 Ultra; Waves 26, 27, 29, 31 and 32 ran on a TECNO KJ7; other waves' device results are recorded in their task specs. Showcase and test callbacks are UI fixtures held in memory: they prove layout and callback wiring, not real certificate verification, rewarded-ad payout, platform screen-reader announcement, memory-leak freedom, 60 FPS or persistence.
0.5.1 #
- Added
PluginAdapterConformanceSuite.verifyAchievementSyncSeamwith five checks for exact round-trip, duplicate uploads without progress inflation, max-progress merging, disjoint ID preservation, and a concurrent-upload probe that also checks previously written IDs remain intact. Throws and timeouts fail the relevant check. - The checklist writes test progress: use an isolated sandbox/test account and a fresh, nonblank
testIdPrefix. Test data is not deleted; a prefix does not protect production data from a broken adapter. Timeouts do not cancel underlying I/O, and passing the concurrent probe does not prove every race schedule.
0.5.0 #
- Added
AchievementSyncSeam(pushProgress/pullProgress) andAchievementSyncCoordinator, a platform-neutral way to syncAchievementServiceprogress with Play Games, Game Center or your own backend. The package ships no backend client; register your adapter withGet.put<AchievementSyncSeam>(adapter, permanent: true). AchievementSyncCoordinator.sync()pulls, merges per achievement withmax(local, remote), then pushes the merged map. Adapters must merge uploads atomically by max on the backend to preserve concurrent device updates. Progress only grows; unchanged merges still persist so retries after a failed save remain durable. A failed pull or push returns a retryableSdkFailure(network); a failed local save returnsSdkFailure(storage). Resetting or lowering progress is not supported by this merge.- Added
AchievementService.mergeProgressDurably(remote)(raises progress, firesonUnlockfor newly completed achievements, awaits the save) andAchievementService.progressSnapshot(read-only copy). Purely additive: no existing signature changed. - Fixed
AchievementServicetrimming nonempty achievement IDs during hydration even though registration and increments preserve them. IDs now round-trip unchanged through storage. Save metadata (schemaVersion,syncedAtMs) is excluded from progress and these two IDs are reserved; registering or incrementing them throwsArgumentError. Save format is unchanged. - Added an
AchievementSyncSeamtile to the example cookbook. Added theworkflow_dispatchtrigger to CI so the gated jobs can be run on demand.
0.4.5 #
- Fixed
InventoryServiceleaving its in-memory state changed when persisting failed.grant,consume,setEquippedandmoveSlotchanged the slots, the next slot id and the transaction ledger before writing, and the storage error was thrown without undoing that. A retry ofgrantwith the sametransactionIdthen returnedSdkSuccessalthough nothing had been saved, and a failedconsumekept showing the old count until an unrelated call exposed the hidden change and wrote it to disk. These methods now restore the previous state and rethrow the same error, so a retry saves for real. Signatures and the thrown error are unchanged. - Tightened tests found weak by mutation checks (
SdkResultequality and hash, the level-up haptic type,ReactiveEnergyBarservice swap, daily quest read failures). No other library code changed.
0.4.4 #
- Fixed
InventoryService.consumechecking each line against the owned quantity on its own, so a request split across duplicate lines (5 owned, "4 then 4") succeeded and left 0. It now sums quantities per item first and rejects withSdkErrorKind.validationwhen the total exceeds what is owned. - Fixed
PlayerProgressionService.grantXpleaving its in-memory state changed when persisting failed. The storage error was already thrown, but the XP total, highest unlocked level and transaction ledger were not restored, so a retry with the sametransactionIdreturnedSdkSuccesswithout anything ever being saved. It now restores the previous state and rethrows the same error, so the retry saves the grant. The method signature and the thrown error are unchanged. - Added behavioural tests for
StorageService,GameSessionController, the diagnostics export bundle,DailyQuestService,InventoryService,PlayerProgressionService,RewardTransactionPipeline,PerformanceTierServiceand several widgets. No other library code changed.
0.4.3 #
- Corrected the README Android setup for scheduled reminders, verified on a Pixel 7 Pro (Android 17): the plugin already merges
POST_NOTIFICATIONSandVIBRATE, so the app does not declare them.ScheduledNotificationReceiveris required for reminders to appear (without it the alarm registers but no notification is posted); the boot receiver and exact-alarm permissions are optional. - Added platform-mocked tests for
AudioManager's background-music paths (start/stop, pause/resume, ducking, volume, mute toggle, dispose). No library code changed.
0.4.2 #
- Fixed
StorageService.getStringListthrowing when the stored JSON list held non-string elements (e.g.[1,2]): the lazycast<String>()escaped itstry/catchand failed on first read. It now returns[]for any wrong-typed content, as documented. - Consumer app generator (
tool/create_consumer_app.dart) now emits a font/sync-conflict regression test and a device integration test, and defaults toroy_casual_kit: ^0.4.1. CI gained aconsumer-smokejob that builds and runs a generated consumer app on an emulator.
0.4.1 #
- Fixed
VersionedJsonStore.syncWith(..., onConflict:)returningSdkSuccesswhen the conflict resolution's storage write or cloud upload failed, silently losing the save. It now returnsSdkFailure(SdkErrorKind.storageorSdkErrorKind.network). A handler that throws still falls back to last-write-wins. - Fixed Baloo2 not loading in consumer apps:
NeonTheme.fontFamilyand thefitFontSizeForLongestWorddefault now use the package-qualified familypackages/roy_casual_kit/Baloo2.
0.4.0 #
- Note:
throttled(window:)now throwsArgumentErrorfor a negative window (previously it silently disabled throttling), andRetryPolicy.delayBeforeAttemptnow throwsArgumentErrorforattemptNumber < 1, arandomValueoutside[0, 1], or a policy with invalidmaxAttempts/jitterFraction, in release builds too (ENH-85).attemptNumber: 0previously returnedDuration.zero. - Fixed
fitFontSizeForLongestWordreturning the first fitting candidate instead of the largest fitting one when passed an unsorted candidates list (BUG-101). - Note:
ObjectPool(maxCapacity:)andfitFontSizeForLongestWord(candidates:)runtime validations are now backed byArgumentError(ENH-85) instead of assert-only checks, meaning invalid arguments will now throw in release builds. - Added
GameAccessibilityAnnouncer+GameAccessibilityEvent(FEAT-99): a locale-aware, rate-limited screen-reader announcer driven byGameEventBusfor gameplay moments (level-up, reward, lives, custom categories). - Added
PrivacyAwareAnalyticsQueue(FEAT-98): a bounded, privacy-first offline analytics queue with serialized persistence and all-or-nothing retry batch uploads. - Added
SeededChallengeService(FEAT-97): deterministic offline daily/weekly seeded challenges, replayable from the same seed. - Added
RemoteContentPack.withHistoryandGameSessionController.withTimeline(ENH-96): a bounded, verified, persisted content-pack revision history with rollback-by-checksum/version, and a privacy-sanitized game-session outcome timeline with JSON export. Strictly additive — no existing public signature changed. - Added independent
AudioManager.setBgmVolume/setSfxVolumewith restore-on-focus/ducking behavior (ENH-95). - Added direct unit/widget coverage for
AdRewardSeam,RuntimeFlags,debug_log, thecommon_widgetsbarrel, the BUG-92PauseOverlayedge case, andRoyCasualKitContractTestKit(IDEA-71). - Hardened CI (BUG-100): audits
example/'s own dependency graph, proves the golden-tolerance comparator's exact 1%/2% boundary, and adds a real-device GameDemo core-loop smoke test. - Generalized
tool/asset_license_check.dartto scan multiple asset roots via--asset-rootssoexample/'s own audio assets are covered too (BUG-99). - Fixed an async lock chain losing mutual exclusion across a waiter's timeout, and made retry timeouts terminal instead of silently retryable (BUG-96).
- Swept all remaining runtime English string literals into the
en/vitranslation maps and confirmed a fresh install always defaults to English regardless of device locale (BUG-94). - Excluded decorative overlays from the semantics tree and made
EnergyBarreactive to its underlying stream (BUG-95). - Fixed
GameEventBussubscriptions leaking past widget dispose in the Game Demo, andAchievementUnlockListeneronly ever subscribing once instead of re-resolving ondidUpdateWidget(BUG-93). - Surfaced storage failures from
AchievementServiceinstead of silently dropping them, and madeEconomyWalletmulti-currency updates atomic via a batch transaction soPrestigeServicecan no longer partially apply (BUG-90). - Fixed
RemoteSchemaDefaccepting reserved Dart keywords as identifiers, a validated schema still holding a live (mutable) reference to the caller's field list, andBouncingOrb's bounce-clamp throwing in a viewport smaller than twice the orb radius (BUG-97).
0.3.1 #
- Added durable verified caching to
RemoteContentPack.withCache(ENH-94): a signature-verified, migrated live-ops pack is persisted only after validation succeeds; a fresh instance prefers that last-known-good cache over its bundled asset, survives an offline restart, rejects tampered/future-schema/wrong-typed cache data, and supports an optional max cache age. The originalRemoteContentPack(...)constructor remains unchanged and cache-free for full source compatibility. - Added reward-plan dry-run preview/execution (FEAT-96):
RewardTransactionPipeline.withInventory,preview, andexecutePlanunify currency (EconomyWallet) and item (InventoryService) rewards into one immutable plan; preview is deterministic and write-free, execute rejects stale state withSdkErrorKind.conflict, and item failures reuse the existing persisted pending/partial/committed +resumePendingflow instead of attempting a fragile compensating rollback. AddedInventoryService.previewGrant,RewardTransactionRecord.withItems, andgrantWithItems; all original constructor/method signatures remain unchanged (API-gate additive only). - Added
VersionedJsonStore.syncWithResult(BUG-84), a non-breaking typed-result companion to the existingsyncWith: cloud download/upload transport failures now returnSdkFailure(kind: network)instead of escaping as unhandled exceptions; the originalFuture<void> syncWithAPI remains source-compatible and degrades to a safe no-op on network failure. - Fixed a race between
StorageService.flush()and a directsetInt/setBool/setDouble/setStringwrite to the same key landing at the same time (BUG-83) — whichever call was invoked later now always lands on disk last, matching real call order. - Fixed
CheckpointCoordinator.restoreLatest()aborting mid-restore when one participant'srestore()throws (BUG-85) — every other participant (including ones registered after the broken one) now still restores, and the failure names exactly which participant(s) threw. - Fixed
RoyCasualKit.initialize()leaving a half-registeredAudioManager/WakeLockServicebehind when that module'sinit()throws (BUG-86) — a retriedinitialize()now cleanly re-attempts instead of finding a broken instance already "registered". - Fixed
SaveSlotManager.deleteSlotremoving metadata before the slot's own data (BUG-87) — a failure mid-delete could previously orphan a slot's data with no metadata pointing at it; data is now removed first, metadata and the active-slot pointer only after that succeeds. - Fixed
RewardTransactionPipeline's internal persist step throwing straight past itsSdkResultcontract on a storage failure (BUG-88) — a failed persist now rolls the in-memory record back to its last known-good snapshot and returnsSdkFailure(kind: storage)instead of leaving a record only RAM believes is committed. - Fixed
OfflineOutboxService.drain()missing an item enqueued while a drain pass was already running (BUG-89) — such an item is now processed within the samedrain()call instead of waiting on a next call that may never come. - Enhanced Game Demo (
example/lib/screens/game_demo_screen.dart): interactive tap gameplay feeds wallet and achievements, 10 taps triggers bonus gems, heavy haptic feedback, celebratoryConfettiOverlay, and unlock banner (Option B). - Injected
nowMstestable clock seam intoRewardTransactionPipeline.grant()(BUG-81): replaces hard-codedDateTime.now()with an injectableint Function()? nowMsparam, aligning with the SDK-wide timestamp seam convention. - Added end-to-end performance/memory optimization pass (Option D4): isolated Flame game/HUD/app bar/FAB/tracked overlays behind
RepaintBoundary, reusedConfettiOverlaypainter allocation state to reduce per-frame GC churn, and added optionalRoyLifecycleCoordinator(trimMemoryOnBackground: true)image-cache trimming on background. - Added D4 proof coverage: lifecycle unit tests, confetti/flame overlay widget tests, GameDemoScreen render-isolation widget test, focused integration test (
example/integration_test/d4_perf_memory_test.dart), and verified a release APK build/install/launch on a physical TECNO KJ7 with no fatal AndroidRuntime crash in smoke logs. - Added non-technical evaluation guide (
README.md,example/README.md) and physical device QA matrix (docs/qa/device_matrix.md) with explicit TECNO KJ7 hardware verification steps.
0.3.0 #
- Hardened runtime boundary limits and idempotency safety across core bounded services (
InventoryService,PlayerProgressionService,RewardTransactionPipeline,ConnectivityCoordinator,OfflineOutboxService): constructor rejects invalidcapacity,transactionCapacity,failuresToGoOffline, ormaxQueueSize(<= 0) viaArgumentError.value, preventing ledger truncation and replay vulnerabilities. - Added release-mode runtime policy validation in
RetryExecutor.runwithout breakingconst RetryPolicyconstructors, returning typedSdkFailure(kind: SdkErrorKind.validation)on invalid attempt counts or delay configurations. - Hardened
SceneTransitionController: wrapped consumer load callbacks in an explicit error boundary, preventing stuck loading barriers and unhandled pointer capture on unhandled exceptions (SceneTransitionPhase.error). - Enhanced
ConsentBannerwith dynamic widget lifecycle awareness (didUpdateWidget), supporting seamless policy version bumps and flow migrations on persistent state without dialog stacking. - Enforced strict teardown lifecycle contract in
CheckpointCoordinator.onClose(): immediately completes all pending debounced checkpoint completers with typed failure, preventing permanently leaked Futures. - Guarded
ConnectivityCoordinatorasynchronous continuations against post-close invocations, eliminatingBad state: Cannot add new events after calling closeand preventing dangling probe loops or rogue queue drains. - Modernized API compatibility tool (
tool/api_compatibility.dart): added full support for modern Dart 3 modifiers (sealed,final,base,interface), typed top-level functions, getters/setters, eliminated 44 private symbol leaks, and corrected semver major release gating.
0.2.4 #
- Added
WakeLockService(lib/core/wake_lock_service.dart) — wrapswakelock_pluswith a persisted on/off preference (StorageKeys.wakeLockEnabled, defaulttrue) instead of unconditionally forcing the screen awake, registered via a newRoyCasualKitModule.wakeLockbootstrap module. Same shape asAudioManager.muted: anRxBool enabled,.maybenull-safe accessor,setEnabled/toggle.example/: replaced the hardcodedWakelockPlus.enable()inmain.dartwith the module + a "Keep Screen On" toggle inSettingsScreen(sameCandyToggleSwitchpattern as the sound/dark-mode toggles); dropped the now-redundant directwakelock_plusdependency fromexample/pubspec.yaml(comes transitively through the root package). Tests:test/core/wake_lock_service_test.dart(9 cases) + 2 newexample/test/settings_screen_test.dartcases (with/without the module registered).api_compatibility.dart check: additive (new export, new enum value) — snapshot regenerated.
0.2.3 #
- Docs/example/CI only, no public API change (
api_compatibility.dart check: unchanged). Addressed integrator feedback that the README/guide was incomplete and the example app didn't cover enough of the kit's surface. README's "What's in the package" overview is now backed by a full "Cookbook" section — a real, compilable-shape usage snippet for everylib/core/service (grouped by the same 9 categories), plus a "Utilities" category forlib/core/utils/; fixed 2 stale class names found while writing it (AppVersionGate→AppVersionGateController,InAppReviewHelper→ the actual top-levelmaybeRequestReviewfunction) and one wrong method name (DisasterRecoverySaveExport.signExport→.sign). Addedexample/lib/screens/cookbook_screen.dart: a new screen exercising every core-service category not already covered by Settings/WidgetShowcase/GameDemo (~25 services — RemoteConfigService, RemoteContentPack, RemoteKillSwitchController, SeasonEventService, DailyQuestService, PurchaseSeam, VersionedJsonStore, CheckpointCoordinator, DisasterRecoverySaveExport, the consent-gated analytics stack, SdkEventSchemaRegistry, SdkHealthReport, DiagnosticsExportBundle, CloudSaveProvider/SecureStorageAdapter via theirFake*reference adapters, PluginAdapterConformanceSuite, AppVersionGateController, GameTimeController, maybeRequestReview, MemoryWatchdog, AssetPreloadCoordinator, RoyLifecycleCoordinator.registerHook, HapticChoreographer), each tile firing one real call and showing its real result in aToastBanner. Addedexample/test/cookbook_screen_test.dart+cookbook_screen_more_test.dart+cookbook_screen_remote_config_test.dart(27 widget tests total covering all 24 tiles, split across 3 files after bisecting aToastBanner-ticker-teardown flake that only reproduced under the full test suite's concurrent isolates — 3 asset-loading tiles in the last file areskip: truewith the bisection written up in-file, same precedent as the existing ConfettiOverlay/Page Dotsskip: truetests; verified working for real via a manual on-device smoke test on a Pixel 7 Pro instead — real service calls, real correct results, zero errors inadb logcat). Added CIquality-gatejob (.github/workflows/ci.yml) wiringaccessibility_audit_check/dependency_sbom_check/asset_license_check/performance_budget_check/deprecation_check— previously-built tools that existed but weren't gated in CI — scoped viadorny/paths-filterto only run whenlib/**/tool/**change, sharing the existingconcurrency: cancel-in-progressgroup so rapid pushes coalesce instead of stacking runs. Added.github/workflows/benchmark.yml: a weekly (free — public repo,ubuntu-latest) GitHub Actions job that boots a KVM-accelerated Android emulator, runstool/performance_budget_check.dart snapshot --device=<id>, regenerates the README's "## Performance" table via the newtool/update_readme_benchmarks.dart(+test/tool/update_readme_benchmarks_test.dart), and opens a PR with the refreshed numbers rather than pushing tomaindirectly.
0.2.2 #
- Docs only, no API change. The 0.2.1
screenshots:pubspec field only feeds pub.dev's sidebar thumbnail/gallery page — it does not embed images into the rendered README body, so the README itself still had zero pictures. Added a "Screenshots" section with<img>tags pointing toraw.githubusercontent.com(the repo is public; pub.dev's README renderer doesn't serve package-local files by relative path, only an absolute URL resolves).
0.2.1 #
- Docs/metadata only, no API change. Fixed
pubspec.yaml'sdescriptionand the README's "Install" section, both of which still described the package as pre-publish/with a stale hardcoded widget count. Rewrote widget-kit mentions to avoid hardcoded counts entirely (they drift immediately as widgets are added). Addedexample/README.md(pub.dev's "Example" tab prefers it over dumping raw source) pointing readers to which example file covers what, rather than shrinkingexample/lib/main.dartinto a toy snippet — it stays a real, full bootstrap on purpose. Addedscreenshots:topubspec.yaml(home/settings/widget-kit/game-demo, light and dark) for the pub.dev listing.
0.2.0 #
- Added
RemoteSchemaDef/generateModelSource/buildMigrationRegistry/verifySignedFixture(FEAT-81,lib/core/utils/remote_schema_compiler.dart, no Flutter dependency) plustool/remote_schema_compiler.dart: compiles a declarative remote-content-pack schema (JSON: pack name, versioned field list) into a self-contained, zero-import, immutable Dart model class (fromJson/toJson, value==/hashCode) — the generated file has no dependency on this package at all, so upgradingroy_casual_kitcan never break a previously-generated one.RemoteSchemaDef's factory constructor is the only validation gate: an invalid schema (duplicate/out-of-order version, duplicate or non-identifier field/pack name, a version with zero fields) never produces an instance, so nothing downstream can generate code from it.buildMigrationRegistrywires the schema's version history into the existingSaveMigrationRegistry(FEAT-37) rather than reinventing chain validation;verifySignedFixturegates a fixture through the exact signature+version checksRemoteContentPackalready applies at runtime (save_integrity.dart'sverifyAndStrip, reject-future-version) before the CLI will generate anything from it. Addedexample/remote_schemas/shop_catalog.schema.json+ its compiledexample/lib/generated/shop_catalog_content.g.dartas a real consumer integration, cross-checked inexample/test/shop_catalog_content_cross_check_test.dart(asset-fallback path viaRemoteContentPackand the generated model's ownfromJsonproduce the identical object for the same fixture). - Added
PrivacyAwareAnalyticsSampler(FEAT-83,lib/core/privacy_aware_analytics_sampler.dart): anAnalyticsProviderdecorator combining consent gating (same default-deny ruleConsentGatedAnalyticsProvideralready enforces), deterministic per-session sampling (an event name is hashed with the session'ssessionIdvia the existingfnv1aHash, so the same event in the same session always gets the same keep/drop decision — never a per-call coin flip that would fragment a funnel), PII redaction via the existingSdkEventSchemaRegistry(FEAT-77) applied before anything else downstream, and a fixed-window rate limiter that drops (never blocks, never buffers) once a window's budget is spent.auditSnapshotexposes cumulative forwarded/dropped-by-AnalyticsDropReasoncounts for a health/debug surface. - Added
PerformanceBudgetMetric/BudgetPolicy/checkPerformanceBudgets(FEAT-80,lib/core/utils/performance_budget.dart, no Flutter dependency) plustool/performance_budget_check.dart(check/snapshotsubcommands, same split astool/api_compatibility.dart): a CI performance-budget gate honest about where each number came from viaMeasurementSource(hostHeadlessvsrealDevice) — a policy requiringrealDevicefails outright (wrongSource) rather than silently accepting a host-measured number as device proof.hostHeadlessmetrics are re-measured fresh everycheckrun from this repo's owntool/object_pool_benchmark.dart(FEAT-48) — its allocation-reduction percentage is deterministic (150 vs 6000 allocations with the default scenario, 97.5%) and reproducible on any runner.realDevicemetrics can't be reproduced headlessly in CI:snapshot --device=<id>runsexample/integration_test/app_boot_test.darton a real, currently-connected device and records its real wall-clock boot time;check(no--device) falls back to whateverrealDevicevalue is already committed intool/performance_budget_baseline.json, gated by each policy'smaxAgeMs(a stale device reading is astaleMeasurementfailure, not a silent pass, forcing a periodic real re-run).regressionTolerancePercentflags a value that got meaningfully worse than the committed baseline even while still inside the hard budget. Seededtool/performance_budget_baseline.jsonfrom a real Pixel 7 Pro run (example_app_boot_wall_ms: 32318ms) — S24U was unreachable at snapshot time, same documented fallback as prior device-smoke sessions. - Added keyboard/gamepad activation to
PressableScale(FEAT-82) — used by nearly every tappable widget in the kit (CommonButton,NeonButton,AvatarFrame,LeaderboardList,CommonListTile,RewardChoicePanel,SoundToggleFab,SegmentedTabBar,CandyToggleSwitch,LevelSelectGrid,DailyLoginCalendarWidget— verified by grep before changing it, not assumed), so this one fix closes a kit-wide gap in one place. Enter/Space/gamepad-A (all bound toActivateIntentby Flutter's own default shortcuts) now triggeronTapwhen focused; a focus ring only ever renders whenFocusManager.instance.highlightModeisFocusHighlightMode.traditional, so a touch-only player never sees any visual change. A widget withonTap: nullgets noFocus/Actionswiring at all (unchanged from before, still not focusable). Also addedFocusTrapScope: wraps an in-tree modal's content (this package's overlays are never real routes, see the Dialog pattern inCLAUDE.md) so it forcefully claims keyboard/gamepad focus the moment it mounts and restores focus to whatever had it right before, the moment it unmounts — retrofitted intoPauseOverlay, closing the exact gap that widget's own FEAT-53 doc comment had flagged as out of scope at the time. - Added
DisasterRecoverySaveExport/SlotExportEntry/RestorePreview/RestoreLogEntry(FEAT-84): multi-slot backup export/restore composingSaveSlotManager(slot metadata/key prefixes),StorageService.exportWithPrefix/importWithPrefix, andsave_integrity.dart's HMAC sign/verify.buildExportrejects wholesale (never partially) if the assembled JSON exceeds a configurable size cap — a partially-included slot's save data would be worse than no export at all.previewRestoreverifies the signature, validatesschemaVersion/shape, and returns a read-onlyRestorePreview(slot names, timestamps, key counts) WITHOUT ever writing to storage, so a corrupt/tampered backup can never mutate a local save.applyRestorerestores slot-by-slot in order, stopping at the first failure and logging every step to arecoveryLog— crash-safe becauseimportWithPrefix's own underlyingimportAll(already shipped) snapshots the whole store before each call and rolls back to it on any exception, so a slot interrupted mid-restore keeps its pre-restore data (last-known-good) rather than a half-written mix, and slots restored before the failure keep their new data. - Added
pseudoLocalize/PseudoLocaleTranslations(FEAT-79,lib/core/utils/pseudo_locale.dart): deterministically transforms every value in a locale's key map into a "pseudo-localized" string — accented vowels (so a hardcoded string that bypassedAppTranslations/.trstays plain ASCII and stands out instantly), ~40% length expansion (stress-tests overflow before a real translator ever touches a key), and[[...]]bracket markers (a truncated render is visually obvious). Registered under the syntheticqps/PLOClocale (same convention Android's own pseudolocalization tooling uses) — never inAppTranslations.supported.example/'sSettingsScreenwires a debug-only (kDebugMode-gated) "Pseudo-locale (QA)" toggle demonstrating it live viaGet.addTranslations/Get.updateLocale. - Added
parsePubspecLock/SbomDocument/classifyLicenseText/auditDependencies(FEAT-78,lib/core/utils/dependency_sbom.dart, no Flutter dependency) plustool/dependency_sbom_check.dart(a genuinely headlessdart runCI check): parsespubspec.lockinto a reproducible (name-sorted) SBOM, classifies each hosted dependency's bundledLICENSEfile (permissive/copyleft/unknown), and cross-checks the result against an optional vulnerability-advisory list (this tool does NOT itself scan for CVEs — no bundled database, no network call —--advisories=feeds in a real scanner's output) and unpinned (any-constraint) direct dependencies. A suppression requires anownerand anexpiresAtMs— once expired, the issue reappears automatically, no permanent unowned exceptions. Running this against the package's ownpubspec.lock(85 resolved packages) found 2 real license-detection gaps in the classifier itself before it settled: the dart.dev/Google BSD-3-Clause boilerplate never spells out "BSD", and several MIT-licensed packages (e.g.uuid) ship the license body without ever titling it "MIT License" — both fixed by matching the actual canonical boilerplate text instead of a license name. The one remaining real finding (dbuson MPL-2.0 — not permissive, not GPL-family, a genuine license the maintainer should review) is left unsuppressed rather than papered over. - Added
SdkEventSchemaRegistry/EventSchema/SchemaValidatedAnalyticsProvider(FEAT-77): validates an event's name/params against a registeredEventSchemabefore it ever reaches a realAnalyticsProvider(FEAT-03). An unregistered event name, a missing required param, or a required param's wrong type all reject the whole event (sanitizedParamsempty); an optional param's wrong type drops just that field; apii: trueparam is ALWAYS redacted regardless of anything else (a schema can't declare a param bothrequiredandpii— that throws at schema-construction time, since it would make the param permanently "missing"); an unrecognized field is dropped or rejects the event per the schema's ownunknownFieldPolicy.migrateupgrades an older client's raw params to the current schema shape before validation (same conventionVersionedJsonStore.migratealready uses).SchemaValidatedAnalyticsProvideris a decorator (composes withConsentGatedAnalyticsProviderthe same way that one already wraps a real provider) that never lets a throwing inner provider crash gameplay — the error goes toCrashReporter.maybeinstead.auditLogis a bounded history of every validation decision;eventSchemaAuditHealthCollector(registry)is an opt-inSdkHealthReport(FEAT-39) section reporting recent rejection activity. - Added
scanAccessibility/AuditRule/AuditViolation/AuditBaseline(FEAT-76,lib/core/utils/accessibility_audit.dart, no Flutter dependency) plustool/accessibility_audit_check.dart(a genuinely headlessdart runCI check, same pattern astool/asset_license_check.dart): source-text-scans a widget tree for 3 statically-checkable accessibility rules — reduced-motion coverage (anAnimationControllerwith noreducedMotionreference, error), tap-target semantics coverage (aGestureDetector/InkWellwith noSemantics/semanticLabel/ExcludeSemantics, warning), and RTL directional safety (a literalleft:/right:onEdgeInsets.only/Positionedinstead ofstart:/end:, warning). Deliberately does NOT attempt contrast or text-scale coverage — both need real rendered pixels/layout, already covered bytheme_contrast_validator.dart(FEAT-68) and the golden matrix runner (FEAT-69) respectively.tool/accessibility_audit_baseline.jsondocuments 11 pre-existing, individually-justified exceptions found by running this against the package's own widget kit (a generic squash/wrapper widget not owning its child's semantics, a physics-driven overlay's world-space coordinates, a debug-only overlay, and — the one genuine near-miss this task's own tool caught —HoldToConfirmButton'sreducedMotionmention living only in a doc comment, not in code, which the scanner's line-comment-stripping fix correctly stopped masking). - Added
RemoteKillSwitchController/KillSwitchState/KillSwitchSource(FEAT-75,lib/core/remote_kill_switch_controller.dart): an emergency remote kill switch for a feature/event/shop/animation, built on top ofRemoteConfigService(FEAT-39/ENH-58). Safety policy: an invalid or missing remote value for a feature can never flip it from killed back to enabled — resolution order is a validly-shaped remote value (bool, or{killed, reason?, version?}for an audited decision) → the last validly-resolved state for that feature (KillSwitchSource.cachedLastKnownGood) → a caller-supplied bundled default (KillSwitchSource.assetDefault,falseif the feature isn't in that map).statesis anRxMap, so a screen already open when ops flips a switch remotely reacts viaObxwithout needing to be rebuilt or torn down.runIfEnabled(featureId, action)makes "don't grant/execute a killed feature" the default path instead of relying on every call site to remember its own guard.auditLogis a bounded (200) history of every decision made, each carrying itsreason/version. - Added
MemoryWatchdog/WatchdogKind/WatchdogEntry/memoryWatchdogHealthCollector(FEAT-74,lib/core/memory_lifecycle_watchdog.dart): a debug-only leak detector for ticker/controller/overlay/subscription/Flame-component resources — a widget/service callstrack()when it creates one andrelease()when it properly disposes it, andorphans()reports anything still tracked (a leak candidate). Ships zero overhead in release the same waydlog()does: every method short-circuits on!kDebugModebefore touching any state, a compile-time-foldable check an AOT release build's compiler dead-code-eliminates past.allow(label)exempts a deliberately-permanent resource from ever being reported — the canonical real example already in this codebase isNeonBg's permanent background-animationTicker(see theCLAUDE.mdtesting gotcha).memoryWatchdogHealthCollector()is an opt-inSdkHealthReport(FEAT-39) section reporting the current orphan count/kinds. - Added
AssetLicenseEntry/AssetLicenseManifest/validateAssetLicenses(FEAT-72,lib/core/utils/asset_license_manifest.dart, no Flutter dependency) plustool/asset_license_check.dart(adart run-able CI check, same headless pattern astool/economy_sim.dart/tool/api_compatibility.dart): walks the package's runtime asset roots (asset/,shaders/) and cross-checks every file againstasset/LICENSES.json, flagging a missing manifest entry, a stale entry (asset deleted but entry remains), an incomplete entry (empty owner/license/source), or a disallowed license (unknown/proprietary/all-rights-reserved/no-redistributionby default, ordistributable: false) — never crashes on a missing/malformed manifest, reports every runtime asset asmissinginstead.asset/LICENSES.jsonnow documents this package's actual 4 shipped assets (bkg.ogg,Baloo2.ttf,neon_glow.frag,aurora_bg.frag). Not wired into.github/workflows/ci.ymlby this task (same reasoning as FEAT-68/69 — see this task's Quyết định). - Added
DiagnosticsExportBundle/DiagnosticsBundleView(FEAT-70): packages anSdkHealthReportsnapshot (FEAT-39), aReplayCapsule(IDEA-42), a redacted config slice, and recent log lines into one{schemaVersion, generatedAtMs, appVersion, sections, errors, truncated}JSON support artifact — signed viasave_integrity.dart's existingsignExport/verifyAndStrip(same conventionReplayCapsule.exportSignedalready uses). Default-deny redaction for theconfigsection, matchingHealthCollectorSpec.allowedKeys's existing convention: passingconfigwithoutconfigAllowedKeysproduces an empty (not full) section — a caller must opt keys in by name. Deliberately never accepts a rawStorageService.exportAll()dump (zero redaction there) as an ingredient. Each of the 4 sections is built behind its own try/catch, and if the assembled JSON exceedsmaxBytes(default 200 KB), sections are dropped least-essential-first (logs, thenreplay, thenconfig;healthkept as long as possible) until it fits, every drop recorded inerrorsandtruncatedset — a partial failure never loses the rest of the bundle.DiagnosticsBundleView.fromJson/fromSignedJsonparse a bundle read-only — parsing never touchesStorageServiceor any registered service, unlikeVersionedJsonStoreimport (which is meant to restore state). - Added a Golden Matrix Runner (FEAT-69,
test/support/golden_matrix.dart— test-only, not exported: it depends onflutter_test, a dev_dependency this package doesn't ship):runGoldenMatrix(tester, builder, {goldenBaseName, matrix, guidelines})pumps a widget through a bounded, deterministic sweep (defaultGoldenMatrix: baseline + 1 variation per axis — dark,vilocale, RTL, 2 text scales, reduced motion — 7 cases total, not a full cartesian product) and fails ontester.takeException()(overflow/render error) or a brokenandroidTapTargetGuideline/labeledTapTargetGuideline(Flutter's own built-in accessibility guideline matchers, reused rather than reimplemented), optionally diffing each case against a deterministically-named<goldenBaseName>_<case.name>.png. Demonstrated end-to-end againstCommonButton(test/widget/goldens/common_button_matrix_golden_test.dart, 7 real golden PNGs) — applying it to the rest of the ~47-widget kit is left as follow-up work, not done in this task. - Fixed
CommonButton's pill height: content-driven height landed at 47.0px, 1px under Android's 48dp minimum tap target — found by the Golden Matrix Runner'sandroidTapTargetGuidelinecheck while building it (real payoff of the new tool, not a review finding). Fixed withconstraints: BoxConstraints(minHeight: 48);ShopItemCard's golden fixtures (the only other golden-tested widget embeddingCommonButton) regenerated to match. - Added
validateNeonThemeContrast/ContrastCheck/ContrastIssue/ThemeContrastConfig(FEAT-68): a WCAG contrast validator forNeonTheme— checksink/inkSoftoncard/cardAlt(normal-text threshold, default AA 4.5:1),mutedoncard/cardAlt(a sanity-only "not literally invisible" check, since WCAG exempts disabled controls from a real contrast bar),lockedBorderonlockedFill(UI-component threshold, default 3.0:1), and every pair ofNeonTheme.gemColors(an RGB-distance heuristic, not a real CVD simulation) across bothdarkstates and bothcolorBlindSafestates — restoring both flags afterward, so calling it never leaves the live theme mutated.validateNeonThemeContrastPairsis the underlying pure function (plainContrastCheckcolors in, noNeonThemedependency), for validating a candidate reskin before importing it. Found 2 real, pre-existing issues in the current palette while building this (documented, not fixed, in this task'sdoc/task/done/FEAT-68-theme-contrast-validator.md):inkSofton light-themecard/cardAltreads at 3.92:1/3.40:1 — under the 4.5:1 AA bar for normal text, though over the 3.0:1 large-text/UI bar — andlockedBorder/lockedFillreads at 1.43:1 regardless ofdark, since neither token has a separate dark variant. No CLI tool ships for this one: unliketool/economy_sim.dart/tool/api_compatibility.dart, a real check needsNeonTheme's actualColorvalues, andNeonThemetransitively importsdart:ui— unavailable to a plaindart runprocess outside the Flutter engine (confirmed by trying it:Error: Dart library 'dart:ui' is not available on this platform) — so this validator's CI coverage lives influtter testinstead, which is already a CI step. - Added
InventoryGrid(FEAT-56): a data-driven grid over anInventorySnapshot(FEAT-44) —itemBuilderis entirely caller-supplied (this package ships no concrete item catalog/renderer), and a tap/drag never mutates anything; the caller's own handler is expected to callInventoryService.consume/setEquipped/moveSlotitself. Cells beyondsnapshot.slots.lengthrender as empty up to an optionalunlockedCapacity(default: no locked cells at all), then locked beyond that —InventorySnapshotitself has no "locked" concept, this is a presentation-only opt-in. Built onGridView.customwith an explicitSliverChildBuilderDelegate.findChildIndexCallbackkeyed byInventorySlot.slotId—GridView.builder's plain delegate has none, so Flutter's sliver reconciliation only compares "the widget now at index N" against whatever Element previously sat at index N and never searches the rest of the list for a matching key; withoutfindChildIndexCallback, a reorder or a removal that shifts a slot to a different index would silently hand its old Element/State to whatever item now lands at that index instead of following its ownslotId— caught by a widget test using a stateful item builder before this shipped, not by inspection. Supports drag-to-reorder viaonReorder(fromSlotId, toSlotId)(also opt-in, disabled by default) built onDraggable/DragTarget. - Added
LevelUpOverlay/LevelUpOverlayController/LevelUpCelebration(FEAT-54): a data-driven level-up celebration overlay — the caller suppliesLevelUpEvents already obtained fromPlayerProgressionService.grantXp(FEAT-43); this widget only presents them, it never callsgrantXp/RewardTransactionPipelineitself. SequencesxpFill → levelPop → rewardRevealper queued level, in ascending order, then either moves to the next queued level or back toidle.LevelUpOverlayControlleris a pure Dart, token-guarded state machine (same shape asSceneTransitionController, FEAT-58) — everyshow()mints a token, and only that token's continuation may mutate state, so a supersededshow()silently drops its own late completion instead of double-firingonComplete.skip()jumps straight toidleand firesonComplete(once, guarded the same way) instead of waiting out the remaining phases — implemented with a real cancellableTimerper phase (notFuture.any([Future.delayed(...), ...]), which never cancels its losing branch and would otherwise leave a real pending timer alive after an early skip). Confetti (reusingConfettiOverlay) and haptic feedback are both optional and automatically skipped under reduced motion. - Added
AdaptiveGameHud/HudSlot/HudBreakpoint(FEAT-52): a HUD-content placement widget that reservestopStart/topCenter/topEnd/bottom/side/overlayslots without a caller hand-rolling safe-area, notch, keyboard-inset, RTL, and breakpoint handling itself. Every edge slot is padded byMediaQueryData.paddingdirectly (never wrapped inSafeArea, which would apply the same inset to every slot even when only one edge needs it);bottomadditionally clearsviewInsets.bottomso an on-screen keyboard never covers it.topStart/topEnd/sideuse Flutter's ownPositionedDirectional, so RTL swaps sides automatically with no custom logic.HudSlot.sidehides entirely belowcompactBreakpointWidth(width-driven, not orientation — an orientation change already changes width, so a separate check would just be a second source of truth for the same decision); a descendant of any slot can read the resolvedHudBreakpointviaAdaptiveGameHud.breakpointOf. An optionalflameViewportBounds(local coordinates) keeps the top/bottom slots outside Flame's actual visible viewport when the game's camera doesn't fill the widget (fixed-aspect-ratio letterboxing) — deliberately vertical-only, since horizontal letterboxing would need to reconcile with RTL start/end and is rare on phones. Never wrapsslotsin its ownObx/GetBuilder, so a slot's own reactive content rebuilds independently of the HUD itself. - Added
ObjectPool/PooledComponent(FEAT-48): a generic acquire/release object pool for reusing allocation-heavy instances (Flame particles/projectiles) in a hot gameplay loop instead of allocating and discarding one every frame.release()throwsStateErroron a double-release or a foreign object this pool never handed out — identity, not==, decides what's currently active, so aToverriding equality can't confuse the check.maxCapacitybounds whatrelease()retains (prewarm()/normal reuse), never whatacquire()can hand out — a burst past capacity still succeeds, just without retaining the surplus past its next release.PooledComponentis a FlameComponentmixin that calls back into its pool automatically fromonRemove(), via a plainvoid Function()callback (attachToPool) rather than holding a typedObjectPool<T>reference, sidestepping Dart's invariant generics entirely.tool/object_pool_benchmark.dart(headless, no Flutter/Flame dependency, cross-checked intest/tool/object_pool_benchmark_test.dart) runs a representative particle-burst scenario through the realObjectPoolclass and prints its allocation reduction versus an unpooled baseline — a pool sized smaller than the scenario's concurrent-alive working set shows zero reduction by design (every release lands past capacity and gets disposed), which the benchmark's own test suite locks in as intended behavior rather than a bug. - Added
OfflineOutboxService/OutboxItem/ConflictPolicy/SyncOutcome/ManualResolution(FEAT-67): a persisted, priority-drained outbox for syncing local changes to a remote authority once back online, built onRetryExecutor(FEAT-35) for transient-failure backoff and, when aConnectivityCoordinator(FEAT-62) is supplied, driven automatically by its online transitions the same way that service drains its own internal task queue. An item is persisted fromenqueue()until the exact call that removes it also persists that removal, so a crash between the remote authority accepting an item and this service recording that leaves the item still queued for the nextdrain()— it always retries with the sameOutboxItem.idempotencyKey, so nothing is ever silently lost; avoiding a duplicate effect on the remote side is the injectedOutboxUploader's own responsibility (a real backend keys on that same id to dedupe a retried request it already applied).enqueue()only auto-drains when aConnectivityCoordinatorconfirms it's online right now — without one, back-to-backenqueue()calls would otherwise race their own fire-and-forget drain attempts against each other and break the priority ordering a singledrain()call guarantees. ASyncConflictresponse is handled perConflictPolicy:rejectdiscards the local change for that sync attempt only (it never reaches back into wherever the change came from — a wallet, an inventory — to undo it, so nothing already granted locally is lost);mergecalls a requiredConflictMergerand re-uploads once more, falling back tomanualinstead of looping forever if that still conflicts;manualparks the item inmanualReviewItems(with the authority'sremotePayloadattached) untilresolveManualpicksManualResolution.keepLocal(re-queues the original payload) oracceptRemote(drops it). A corrupt save resets to an empty outbox instead of throwing, the same fail-safe posture as this kit's other ledger-style services. - Added
InventoryService/ItemDefinition/InventorySlot/InventoryLine/InventorySnapshot(FEAT-44): a generic slot-based inventory (consumables + equipment) built on the same idempotent-ledger pattern asEconomyWallet(FEAT-31)/RewardTransactionPipeline(FEAT-42) — every mutating call (grant,consume,setEquipped,moveSlot) runs underAsyncActionGuard.runExclusive('inventory', ...), so concurrent calls serialize deterministically instead of racing, and a repeatedtransactionIdongrant/consume(even across a restart) is a no-op. Agrant/consumecall takes a list ofInventoryLines and applies them atomically against a scratch copy first — if any single line would exceedcapacity(grant) or ask for more than is on hand (consume), the whole call is rejected and none of the lines mutate the real inventory, not just the failing one.grantfills existing under-maxStackstacks of the same item before opening a new slot.ItemDefinitionis entirely caller-supplied (no concrete item list baked into the kit) — an unknown item id in agrantline is rejected before any mutation, and a persisted slot referencing an item id no longer in the current catalog (a stale save from before an item was removed/renamed) is silently dropped on load rather than crashing or granting a replacement; a fully corrupt save resets to an empty inventory the same wayEconomyWalletdoes.setEquipped/moveSlotare naturally idempotent (no transaction id needed) and target a slot's stableslotId, which survives other slots shifting around from a grant/consume elsewhere in the same inventory. - Added
PlayerProgressionService/LevelDefinition/PlayerProgressionSnapshot/LevelUpEvent(FEAT-43): a versioned XP/level SSOT built on the same idempotent-ledger pattern asEconomyWallet(FEAT-31) andRewardTransactionPipeline(FEAT-42) —grantXp(amount, transactionId)runs under anAsyncActionGuard.runExclusivekey, so concurrent grants serialize instead of racing, and a repeatedtransactionId(even across a restart) is a no-op that returns the current snapshot rather than double-counting.level/xpIntoLevel/xpToNextLevelare never persisted directly — only lifetimetotalXpEarnedis, and the rest is always re-derived from it against the curve (purelevelForTotalXp/xpIntoLevelForTotalXp), so they can never drift out of sync with a curve change.validateLevelCurverejects a brokenLevelDefinitionlist (duplicate/missing level number, a non-final level withxpToNext <= 0, a decreasing curve, or cumulative XP overflowing the supported range) at construction time — a bad config throwsArgumentErrorimmediately rather than producing silently-wrong level math later; the curve's last entry must havexpToNext == 0, which is what marks the max level. AgrantXpcall that crosses several level thresholds at once (a big XP grant, an offline-earnings catch-up) grants each newly-reached level'sunlockRewardLinesexactly once through an injectedRewardTransactionPipeline(deterministic'level_up:$level'transaction id) and firesonLevelUponce per level in ascending order; XP past the curve's max level is still tracked intotalXpEarnedfor analytics honesty, butlevelitself is capped and no further unlock fires. A corrupt/malformed save resets to a fresh level-1 state (never throws, never trusts a partially-decoded value upward) — same fail-safe posture asEconomyWallet's own hydrate. - Added
SceneTransitionController/SceneTransitionOverlay/SceneTransitionPhase(FEAT-58): a token-guarded scene-transition state machine (idle → covering → loading → revealing/error → idle) plus the in-tree overlay widget that renders it — sameNeonDialog-style in-tree-overlay pattern (works over a full-screen FlameGameWidget, whereNavigator/showDialogcan't). The controller is pure Dart (noAnimation/BuildContext), unit-tested as a state machine before any animation code was written: everyrun()mints a token, and only that token's continuation may mutatephase/progress/lastError— a still-in-flightrun()superseded by a newer one (rapid re-navigation), or one still resolving aftercancel()/dispose(), silently drops its own late completion instead of stomping the current state or leaving a stuck pointer-blocking barrier.progressonly reflects the active transition. On failure, the overlay showsRetryErrorState.fromSdkFailurewired to an optionalonRetry; the underlying scene isExcludeSemantics+IgnorePointer-guarded for the whole time the transition isn'tidle, so a stray tap/screen-reader focus can never reach the scene being replaced, and the barrier releases the instantphasereturns toidle. RespectsMediaQuery.disableAnimations(or an explicitreducedMotionoverride) by collapsing the cover/reveal fade to zero duration instead of animating.runWithAssetPreloadwires the controller directly to a realAssetPreloadCoordinator(FEAT-47) +GameSessionController(FEAT-41): preload progress feedsprogress, and a successful preload callsmarkReady()+start()on the session so the transition and session phase land in sync. - Added
AssetPreloadCoordinator/AssetManifestItem/AssetKind(FEAT-47): a typed, dependency-aware asset preloader — no concrete Flame/audio/Flutter-asset/shader API baked in, a consuming app injects aloader/unloaderfunction pair that dispatches onAssetManifestItem.kind.preload()validates the manifest up front (adependsOnreferencing a missing id, or a dependency cycle, both return a typedSdkFailure(kind: validation)before any loader call runs, via DFS 3-color cycle detection). Items load in dependency-ordered waves, each wave bounded bymaxConcurrentin-flight loader calls.progress(0.0–1.0, exposed as aRxDouble) only advances for an item that finishes in a way that lets the scene proceed — success, or an optional item's failure; a required item's failure (and everything that transitively depends on it, propagated through the wave graph) is excluded, soprogresscan never falsely reach 1.0 while a required asset is missing — a caller must check the returnedSdkResult, not just watch forprogress == 1.0. An item id already loaded by an earlierpreload()call is never loaded twice — the coordinator tracks a reference count per id,unloadScene()decrements it for the last-preloaded manifest and only callsunloaderonce a shared asset's count reaches 0 (so 2 scenes sharing one background texture don't unload it out from under each other).cancel()stops starting new waves but never abandons an in-flight loader call (nothing leaks);retryFailed()simply re-runspreload()against the last manifest — the same cache naturally skips everything already loaded, so only the previously-failed items (and anything they blocked) actually re-attempt. - Added
PluginAdapterConformanceSuite/ConformanceReport(FEAT-66): a shared conformance checklist a consumer runs against their own concrete implementation of this kit's platform-neutral seams (AnalyticsProvider,CloudSaveProvider,SecureStorageAdapter,CrashReporter,PurchaseSeam— no "ads" adapter, since this kit ships no ads mediation seam by product decision) without importing any vendor SDK. Every check is independent and never throws itself — a check that would have thrown is recorded as afalseresult instead, so one broken check never aborts the rest of the checklist. Every check (including plain "does not throw" ones) is bounded by a per-calltimeout, so a hanging adapter can never block the checklist regardless of which check catches it first — found and fixed exactly this bug during TDD (an earlier version only timeout-guarded the checks explicitly testing for timeout, so a hung adapter blocked an unrelated "does not throw" check for the full 30s test-runner timeout instead of failing fast).SecureStorageAdapter's checklist includes a real privacy check:delete/clearmust leave no residual readable value, not just "not throw". ShipsFakeCloudSaveProvider/FakeCrashReporter/FakePurchaseSeam(in-memory reference implementations, alongside the existingFakeSecureStorageAdapter/NoopAnalyticsProvider) that pass their own suite and double as example implementations. - Added
PlatformCapabilityRegistry/PlatformCapabilitySnapshot/detectPlatformCapabilities(FEAT-71): a typed, cached capability snapshot (haptics/shaders/notifications/background audio) for Android/iOS/web/desktop, computed once fromkIsWeb/defaultTargetPlatform— no platform channel calls beyond what Flutter itself already resolves at process start. Ships a documented, deliberately conservative compatibility policy (every capability assumed unsupported on web; haptics also assumed unsupported on desktop, since there's no OS haptic API there) rather than a hard platform fact — a consumer that knows better for its target can construct the registry with an explicit override snapshot.withFallbackgives call sites an explicit, testable fallback policy instead of silently skipping or crashing on an unsupported platform; a real error thrown by either branch still propagates normally.detectPlatformCapabilities'sisWeb/targetPlatformparams are injectable, so its device-matrix test fixtures every platform combination without needing a real device per platform. Demoed inWidgetShowcaseScreen, including a "fire haptic with fallback" button. - Fixed
CommonButtonpills having zero horizontal padding — long labels could visually touch/crowd the pill's rounded edges instead of having proper inset. Addedhorizontal: NeonTheme.s16alongside the existing vertical padding; regenerated the 2 golden images this shifted (shop_item_card_basic.png/shop_item_card_ribbon.png). - Added
AppSessionTracker/SessionInfo(FEAT-63): session id, install/session-sequence counters, and foreground duration in one place instead of every feature hand-rolling its own bookkeeping. One session per process —sequenceincrements exactly once per construction (cold start); a hand-edited/corrupt negative persisted sequence clamps to 0 before incrementing, never propagating a negative value forward. Foreground duration is measured with an injectableStopwatch(real elapsed time, not wall clock — a duration measurement doesn't care about clock anomalies) and excludes backgrounded time entirely. A resume withinsessionTimeout(default 30 min) of backgrounding continues the same session; a resume after longer starts a brand new one (newsessionId, bumpedsequence, foreground duration reset) — the standard mobile-analytics "session timeout" convention.analyticsContext()returns{}unlessConsentCategory.analyticsis currently granted on the registeredConsentStateService(FEAT-61) — the tracker never sends anything itself, but a caller attaching this to an analytics event can't leak session/install data pre-consent. Demoed inWidgetShowcaseScreen. - Added
AppVersionGate/GateDecision/AppVersionGateConfig/evaluateVersionGate/compareAppVersions/AppVersionGateController(FEAT-59): soft/force update and maintenance-mode gating from remote config, with a fully safety-first policy — any invalid input (an unparseable current version, or an unparseableminimumVersion/recommendedVersion) degrades toGateDecision.okrather than ever force-blocking a user for a corrupt config; only a plainmaintenanceActivebool (can't be "invalid") can force/maintenance-gate.compareAppVersionsdocuments its own semver policy: prerelease sorts before the same core release (1.2.3-beta<1.2.3), build metadata (+001) is ignored entirely.AppVersionGateControllerreads straight fromRemoteConfigServicerather than keeping its own cache — that service already gives "asset default, remote merges in, remote fail/corrupt keeps last-good" for free (ENH-58). ShipsAppVersionGateOverlay: force/maintenance block the back button (PopScope(canPop: false)) and aren't dismissible; soft update is dismissible and reports back viaonSoftDismissso the caller can persist the cooldown; a failed injectedlaunchStorecall shows an inline error and a "Retry" button instead of looping. Demoed inWidgetShowcaseScreen. - Added
DeepLinkCommandRouter/DeepLinkRoute/DeepLinkCommand/parseDeepLink(FEAT-60): parses deep links into typed commands against an allowlist of routes (scheme/host/path shape — a:namepath segment captures a param) and dispatches them to registered handlers, with no concrete deep-link plugin dependency — a consuming app feeds its own URI stream (e.g.app_links) intohandleUri.parseDeepLinkis a pure function (fixture-testable) that rejects an oversized URI, an unrecognized scheme/host, or a path that matches no route — never throws. A link arriving beforemarkReady()is queued (never dropped) and drains exactly once when the app signals it's ready; a duplicate call for the same URI withindedupeCooldownis ignored, deterministically even for 2 concurrent calls (the cooldown is stamped before any handler awaits). Every handler registered for a command type runs (highestregisterHandlerpriority first) even if an earlier one throws — captured per-entry in a diagnosticDeepLinkHandlerResult, never rethrown, and never affecting a different command's dispatch. Demoed inWidgetShowcaseScreenand wired into the example app's realAndroidManifest.xmlintent-filter (roycasualkit://open/...) viaapp_links. - Added
ConnectivityCoordinator/ConnectivitySignal/ConnectivityState/QueuedTask(FEAT-62): a reactive Internet-reachability coordinator with a bounded, idempotent retry queue.ConnectivityState.onlineis reported only once an injectedprobe(e.g. a HEAD request to a known-good endpoint) actually succeeds — aConnectivitySignalreporting a network interface is up is never, by itself, treated as online (a captive portal or DNS outage would otherwise false-positive). Interface flapping is debounced before evaluation, and a single failed probe while previously online moves toConnectivityState.degradedrather than straight tooffline(hysteresis) — onlyfailuresToGoOfflineconsecutive failures declare it truly offline, so flapping connectivity can't spam the UI.enqueued tasks are capped atmaxQueueSize(evicts the lowest-priority entry once full), keyed byidempotencyKey(a duplicate replaces, never duplicates), and drain automatically once back online — highest priority first, through an injectableRetryExecutor/RetryPolicy(FEAT-35) per task; one task exhausting its retries is dropped without blocking the rest of the queue, and a mid-drain drop back offline stops the drain immediately, leaving the rest queued.connectedStreambridges directly intoNetworkStatusBanner.stream. Timers use the same injectablecreateTimerseam asCheckpointCoordinator/HapticChoreographer, andonClose()cancels every timer/subscription — no leaks. Demoed inWidgetShowcaseScreenalongside aFakeConnectivitySignaltest double. - Added
ConsentStateService/ConsentCategory/ConsentStatus(FEAT-61): an SSOT for privacy consent gatinganalyticsandpersonalization(noadscategory — this kit ships no ads mediation seam, a deliberate product decision from FEAT-02's rejection). Default-deny: a category never explicitly decided reads asConsentStatus.unknown, nevergranted, and a corrupted or hand-edited persisted entry is dropped rather than coerced, so it also reads as unknown — never an accidental early unlock.policyVersionsupport: a category persisted under an older policy version reads back asunknownregardless of its prior decision, forcing a fresh review after a privacy-policy bump.grant/deny/resetpersist immediately and take effect immediately within the same instance. ShipsConsentGatedAnalyticsProvider, a decorator over any realAnalyticsProviderthat only forwardslogEventwhile analytics consent is granted — a consuming app registers it in place of its real provider so every call site automatically respects consent. Demoed inWidgetShowcaseScreen, including gatingExperimentBucketingService's variant assignment behind personalization consent. - Added
PersistentCooldownService/CooldownStatus/CooldownSnapshot(FEAT-36): a keyed cooldown tracker (reward claim timers, booster reuse delays, PvP action gates, ...) that survives app restart and resists clock-rewind cheating — every read is computed lazily fromnowMsClamped()against a persistedkey -> endMsmap, the same anti-cheat clampEnergyService/daily rewards/streaks already use. There is deliberately no timer of any kind in the service, per-key or shared: N active cooldowns cost zero background timers, the same zero-timer designEnergyServicealready uses for its own regen math.start/cancelvalidate their input (empty key, non-positive duration) and persist immediately (not write-behind-buffered, since a missed write on kill would let a booster be reused for free); a corrupted or hand-edited per-key entry is dropped rather than trusted, so it reads as never-started instead of coercing into an early unlock. ShipsCooldownCountdownChip(presentation/widgets/common/), a thin adapter overCountdownChipthat takes plainremaining: Durationdata (same convention asEnergyBar/LevelSelectGrid— widgets take data, the caller owns the GetX service) and renders nothing while a cooldown is ready. - Added
DeprecationRegistry/DeprecatedApi/DeprecationStatus(FEAT-85): a semver-aware deprecation tracker for the SDK's own public API lifecycle. EachDeprecatedApientry pairs with a real@Deprecated('...')annotation on the code and adds structured, machine-checkable data an annotation string alone can't carry —deprecatedInVersion,removeInVersion(validated to always be afterdeprecatedInVersion), and amigrationHint.DeprecationRegistry.checkAll(currentVersion)reports each entry asactiveorpastGrace(removal-version boundary is inclusive);pastGraceOnlyfilters to just the ones that should already be gone. Ships withtool/deprecation_check.dart, a standalonedart runscript that reads the package's ownpubspec.yamlversion and its ownkPackageDeprecationslist and exits non-zero once something is past its removal version — ready for a consumer or this repo's CI to wire in when they choose (not wired into.github/workflows/ci.ymlby this change). - Added
SdkHealthReport/HealthCollectorSpec/defaultHealthCollectors(FEAT-39): a QA/support-facing diagnostic snapshot collector registry, safe to export/share by default. EachHealthCollectorSpecdeclares its own key allowlist — any key itscollect()returns that isn't on that list is silently dropped (default-deny, not default-allow), so a copy-pastedtoken/secret field can't leak into a report a support person might screenshot. A collector that throws or exceeds its timeout only fails its own section (_error), never the rest of the report; string/list values are size-capped so a report can't balloon.defaultHealthCollectors()ships one collector per app/audio/performance/remoteConfig/replay-buffer module, each reportingregistered: false(not an error) when that module isn't wired up in the consuming app. Wired intoDebugQaOverlay's existing debug/profile-only gate as a new "Health" tab. - Added
RetryPolicy/RetryExecutor/RetryOutcome/RetryAttemptEvent(FEAT-35): a reusable exponential-backoff-with-jitter retry for cloud save/config/analytics calls, so consumers stop hand-rolling delay loops.RetryPolicyis pure math (immutable, computed entirely indoublemicroseconds so a huge attempt number clamps tomaxDelayinstead of overflowing);RetryExecutorruns the actual loop with injectabledelayFn/randomFn(same seam-injection convention asHapticChoreographer'screateTimer), so tests drive it deterministically without real delays.retryIflets a caller stop immediately on a non-transient error;isCancelledlets a caller abort mid-loop; the final failure'scause/stackTraceare always preserved, never swallowed. - Added
SecureStorageAdapter/SecureStorage/FakeSecureStorageAdapter(FEAT-40): a platform-neutral secure-storage seam (no concrete SDK pulled in — a consumer registers their own adapter, e.g. wrappingflutter_secure_storage) with noNoop*default on purpose, same reasoning asPurchaseSeam— silently no-op'ing a secret write would hide a real integration bug.SecureStorage.read/write/delete/clearadd the typedSdkResultcontract, key validation, and — critically — a clear typed failure when no adapter is registered rather than ever falling back to plainStorageService/SharedPreferences for something meant to be secret.FakeSecureStorageAdapteris an in-memory test double for consumer contract tests. - Added
GameTimeController/GameClock(FEAT-49): a gameplay clock kept separate from wall clock, so pause/slow-motion/replay never desyncs a game's own timers.GameClockis a pure, no-Flutter-dependency core (mirrorsPerformanceTierService'sFrameBudgetTrackersplit) —advance()clamps a negative or huge (backgrounded-for-minutes) delta, appliesscale(rejecting a non-finite/non-positive value without touching the existing scale), and supports an optional fixed-step accumulator for deterministic sub-stepping.GameTimeControllerbridges it to Flame'supdate(dt)loop viatick()and to Flutter via theelapsedRx; when aGameSessionControlleris wired in, ITS pause state is the sole authority (no second, competing pause flag), so double pause/resume between systems can't happen. - Added
CheckpointCoordinator(FEAT-46): coalesces multiple game systems' state into one atomic, versioned save checkpoint on top ofStorageService— one JSON blob under one key, so a commit can never be observably "half applied" across participants the way separate per-system keys could be if killed mid-sequence.requestCheckpoint()debounces frequent calls (position/score ticks) into one flush after a quiet window;critical: truecancels that debounce and flushes immediately. ARoyLifecycleCoordinatorhook flushes immediately on backgrounding. Keeps the previous commit as a fallback generation —restoreLatest()tries the current commit first, then that fallback, so one corrupted write doesn't lose the whole checkpoint; a participant'ssnapshot()throwing (or a non-JSON-encodable aggregate) aborts the whole flush before anything is written, never a partial/mixed commit. - Added
AsyncCommonButton(FEAT-50): aCommonButtonwrapper that drives itself through idle/loading/success/error around an asynconPressed— the state machineBackupRestorePanelhand-rolled per-widget before this existed. Guards double-tap/re-entry, supports an optionaltimeout, auto-reverts to idle aftersuccessDuration/errorDuration, respectsNeonTheme.reducedMotion. - Added
RewardTransactionPipeline/RewardLine/RewardTransactionRecord/RewardSource(FEAT-42): orchestrates a reward grant (ad/IAP/quest/daily-login) across multiple currency lines on top ofEconomyWallet's existing per-line idempotency — groups lines under one logical transaction id (each line gets its own derived wallet transaction id so 2 lines never collide), keeps a source-tagged persisted audit trail, andresumePending()finishes a multi-line grant interrupted mid-way. A post-commit analytics hook can never roll back an already-applied reward. Earn-only by design;grantFromDailyQuest/grantFromDailyLogin/grantFromPurchaseare thin adapters over the genericgrant(). - Added
StorageService.eraseAll()/removeAllWithPrefix(prefix)(IDEA-55/IDEA-56):eraseAll()names the classic "Reset progress" Settings action self-explanatorily instead of relying on the undocumentedimportAll(const {})trick;removeAllWithPrefixwipes every key starting with a prefix (e.g. one save slot's namespaced keys) via the same rollback-on-error_replaceAllpath, and rejects an empty prefix (which would otherwise silently behave likeeraseAllunder a name that doesn't say so). - Added
SaveSlotManager/SaveSlotMeta(IDEA-56): manages multiple independent save slots (separate characters/profiles) on the same device —createSlot/listSlots(most-recently-played first)/renameSlot/touchSlotfor metadata,activeSlotId/setActiveSlotfor which slot is current, andkeyFor(slotId, suffix)handing out a namespaced storage key for a caller's OWNVersionedJsonStore(this never defines the player-profile schema itself).deleteSlotremoves a slot's metadata and every one of its namespaced keys viaStorageService.removeAllWithPrefix— no leftover keys, and it becomes the newactiveSlotId: nullif it was the active one. - Added
GameSessionController.maybe(ENH-65): brings it in line with every other service inlib/core/. - Added
loadingtoShopItemCard(ENH-66): forwarded straight to the "Buy"CommonButton(IDEA-53) — shows a spinner and blocks double-tap while an in-flight purchase is pending. - Fixed
RemoteContentPacktreating a missingschemaVersionas "already current" (skippingmigrateentirely) instead of "version 0" likeVersionedJsonStore(BUG-37) — a live-ops content author forgetting the field could silently end up with stale content and no error. - Added
OnboardingCoordinatorService(IDEA-54): decides WHICH onboarding/tutorial flow (registerFlow,nextEligibleFlow) should run next and whether one has already been seen (isFlowSeen/markFlowSeen, monotonic per flow version) — the coordination layer this package's tutorial widgets (TutorialSequence,SpotlightOverlay, IDEA-35's JSON-authored flows) never had. Never renders anything itself or touchesBuildContext/Navigator— only answers "which flow, if any, should run right now"; a caller still owns showing the actual UI. Bumping a registered flow's version makes it eligible again exactly once, independent of every other flow. - Added
ReplayRecorder/ReplayCapsule/ReplayEvent/findFirstDivergence(IDEA-42): a bounded ring-buffer recorder for gameplay/input events, packaged (with the session's RNG seed, built on FEAT-45'sSeededRandom) into a versioned, optionally-signedReplayCapsulea developer can import elsewhere to reproduce a bug that was hard to describe from a screen recording alone.record()is O(1) (a fixed-size circular buffer write, no I/O) so it's safe on a gameplay hot path; a corrupt/tampered/future-schema capsule is rejected outright (null, never throws) rather than partially trusted.findFirstDivergencecompares 2 ordered outcome lists (a recorded session vs a fresh replay) and reports the first point they differ, with enough context to debug why.DebugQaOverlaygained a Replay tab (start/stop/export) wired to it. - Added
SeededRandom/RandomSnapshot/CompiledWeightedTable/SeededRandomService(FEAT-45): a deterministic, cross-platform-stable RNG —SeededRandomimplementsdart:math'sRandom(so it's a drop-in source forweightedRandomPick/List.shuffle/anything else expecting aRandom), but unlike it exposes its internal state viasnapshot()/SeededRandom.fromSnapshotfor exact resume, and uses hand-rolled 32-bit-safe arithmetic so the same seed + call sequence produces the same output on every Dart compile target (a naive 64-bit generator would silently drift between the VM and a web build).SeededRandomServicederives independent, namespaced streams (stream('loot'),stream('enemy_spawn'), ...) from one root seed via FNV-1a, so one system calling its stream an extra time never shifts another system's sequence.CompiledWeightedTablepre-validates a loot table once instead of everypick()call. Also extractedfnv1aHash(previously private toExperimentBucketingService) into its owncore/utils/fnv1a.dartutil now that a second caller needs it. - Added
EconomyWallet.maybe/RoyLifecycleCoordinator.maybe(ENH-60): the only 2GetxServices inlib/core/that were missing the static.maybegetter every other service already has (same gap shape as ENH-19'sCloudSaveProviderfix) — both were added after that sweep. CircularProgressRing/WheelSpinnergained proper Semantics (ENH-59): both are drawn entirely viaCustomPaint, and previously exposed nothing to a screen reader (CircularProgressRing's optionallabel/iconstill got their own automatic semantics, but never conveyed "this is a progress indicator" or its percentage).CircularProgressRingnow announces "Progress: N%" (overridable via a new optionalsemanticLabelparam) plus a matchingvalue.WheelSpinnernow announces "Spinning" while animating and "Stopped on {segment label}" once settled, as a live region so the landed result is announced without needing focus moved back to it.AudioManager.playSfxgained aduckparam (IDEA-45): when true, bgm volume automatically drops for the duration of that SFX and restores once it (and every other concurrently-playing ducked SFX) finishes — a counter (exposed read-only asAudioManager.duckCount, so a caller's own UI can show a "ducked" indicator), not a bool, so overlapping ducked SFX never leave bgm stuck quiet after only the first one ends. Restore isn't gated onmuted, so muting mid-duck and unmuting later never leaves bgm stuck at the ducked volume.- Added
QuestBoardPanel/QuestViewModel(IDEA-44): the ready-to-use on-screen counterpart toDailyQuestService(IDEA-29), which previously had no widget consuming it. Shows a list of quests (label + progress bar + Claim button), same pure/data-driven convention asLeaderboardList— the caller maps its ownDailyQuestServicestate intoQuestViewModels rather than this widget talking to the service directly. Claim is only enabled once a quest is completed and unclaimed; a quest crossing into that state at runtime pops its Claim button in with a small celebratory bounce (never replayed if already claimable on first mount), respectingNeonTheme.reducedMotion. - Added
AchievementService.onUnlockandAchievementUnlockListener(IDEA-43):onUnlockis a broadcastStream<String>firing exactly once per achievement id the momentincrementProgresspushes it from not-completed to completed (never fromregister, even if a newly-declared threshold is already retroactively met; never again on further increments).AchievementUnlockListenerwraps a subtree and shows aToastBannerfor every unlock — the first widget in the kit to actually consumeAchievementService, which previously had none. - Added
RemoteContentPack<T>(IDEA-37): a typed, signed remote content slot for live-ops content (level definitions, shop catalogs, event schedules) built onRemoteConfigService's asset-fallback-then-fetch pattern,VersionedJsonStore's schema-versionmigrate, andsave_integrity.dart's HMAC signing.load()always resolves from the bundled asset first — never network-gated — then afetchRemotefetch (if supplied) refreshescurrentin the background once it resolves. A fetched envelope with a bad/missing signature, a futureschemaVersion, or a shapefromJsoncan't read is rejected silently, keeping whatever content was already applied — never crashes, never applies unverified content. A pack with nocontentSecretconfigured never trusts a signed envelope at all (no key to check it against). DebugQaOverlaygained a Playground tab (IDEA-39) alongside its existing read-only state dump — a QA tester/designer can live-preview aCommonButtonwith an adjustable label, variant, and color directly on-device (long-press the corner, switch tabs), no rebuild needed.- Added
TrustedClockService/classifyClockSample(IDEA-40): a rewind-proof clock that also RECOVERS from a suspicious forward jump, unlikeutils/clamped_clock.dart's plain watermark (which permanently locks onto a far-future jump — deliberate or an honest clock misconfiguration — until the real wall clock naturally catches up, which for a big jump can be months or years). Classifies each newClockSample(wall + monotonic reading) as normal/rewind/suspiciousForwardJump/reboot; a rewind or a quarantined jump never advances the trusted baseline, so it stays comparable against reality instead of a bogus reading. Migrates from the legacyStorageKeys.maxMsSeenwatermark on first use. Standalone and fully tested — not yet wired into any existing time-gated service (see the task's## Quyết địnhfor the scope reasoning); an optionalTrustedTimeSourceseam lets a caller force-reconcile past a quarantine when a confirmed trusted timestamp is available. - Added
HapticChoreographer/HapticPattern(IDEA-41): a small data-driven haptic sequencer on top offireHaptic—HapticPattern.reward/combo/errorpresets (plus custom patterns, bounded to 16 pulses / 5s total for safety) play back pulse-by-pulse through an injectable timer.play()always cancels whatever pattern is still running first, so rapid re-triggering ("rage tap") never stacks multiple patterns' pulses on top of each other. - Added a headless economy/balancing simulator,
tool/economy_sim.dart(IDEA-36) —dart run tool/economy_sim.dartfast-forwards a scripted session pattern (sessions/day, energy spent/session) throughEnergyService's regen math andOfflineProgressionService's earnings math, printing the resulting energy/currency curve across simulated days, no Flutter runtime required. Extracted the 2 services' pure formulas intocore/utils/economy_math.dart(regenEnergy/offlineEarnings) so both the real services and the simulator call the exact same functions — verified by a cross-check test running both side by side against an identical timeline. TutorialSequence/TutorialStep(IDEA-35): data-driven tutorial authoring.TutorialStep.listFromJson(json, keyRegistry: ...)parses a JSON step list (the typical source:RemoteConfigService.getString('onboarding_flow_v1')) intoList<TutorialStep>for the existingTutorialSequenceController.start— a designer can now reorder/reword/A-B-test onboarding without a rebuild. A step referencing an unknowntargetKeyname is skipped, never crashes.TutorialStepgained an optionalid; when set,TutorialSequenceControllerlogstutorial_step_shown/tutorial_step_dismissedthroughAnalyticsProvideras the sequence advances (an imperative step with noidlogs nothing, unchanged from before).- Added
SeasonEventService(IDEA-34): a repeating, has-a-time-window event/season schedule forCountdownChip.currentWindow(eventId, {length, cooldown})returns the sameSeasonEventWindow(start/end) for repeated calls within onelength+cooldowncycle, including across app restarts (anchor persisted pereventId), and never rolls a window back to an earlier cycle when the device clock is wound back (ClampedClock-backed). - Added
PurchaseLedgerService(IDEA-33): a thin local cache for the 2 purchase shapes almost every game needs on top ofPurchaseSeam—grantConsumable/consumefor a balance that can never go negative (consumereturnsfalseinstead of overdrawing),grantPermanent/ownsfor one-time unlocks. Not a receipt-validation layer — meant to be called by the app's ownPurchaseSeamadapter only after it has already verified a purchase. - Added
ExperimentBucketingService(IDEA-32): stable A/B-test variant assignment on top ofRemoteConfigService's flat key/value config.variantFor(experimentKey, variants)hashes a cached per-device anonymous id (generated once, never re-rolled) against the experiment key via a dependency-free FNV-1a hash, so the same device always lands in the same bucket for a given experiment across every app restart. - Added
BackupRestorePanelwidget (IDEA-31): putsStorageService.exportAll()/importAll()andsave_integrity.dart's HMAC sign/verify on screen. Caller suppliessecret,onExport(signedJson)andonImport() -> signedJson?seams (the actual file/QR/share mechanism stays app-specific, same convention asCloudSaveProvider); a checksum-mismatch or malformed import shows an inline error instead of crashing, and a restore is gated behind a confirm dialog since it overwrites the whole local save. - Added
LocalScoreboardService(IDEA-30): a local, single-device high-score table.submitScore(playerLabel, score)ranks a new submission against every previous one on the device (ties break by earlier submission);topN(n)returnsLeaderboardEntrydirectly (rank + formatted score viafmtNum), ready to feed straight intoLeaderboardListwith no caller-side transform. Capped (default 50 rows) — lowest score dropped as soon as it would rank last. - Added
DailyQuestService(IDEA-29): a caller-declared daily/weekly quest tracker — the third reset-cadence mechanism alongside the permanentAchievementServiceand the fixed-calendarDailyLoginService.register(id, targetCount, {period})declares a quest once;incrementProgress/isCompleted/claimtrack it perQuestPeriod(daily/weekly), resetting automatically on theClampedClock-backed period boundary so winding the device clock back can't re-claim an already-claimed quest. RemoteConfigService(ENH-58): partial remote responses now merge key-by-key on top of asset defaults instead of replacing the whole config (a response missing/omitting a key no longer wipes its asset fallback); added a read-onlysnapshotgetter and asourcegetter (RemoteConfigSource:assetOnly/remoteMerged/remoteFailed) to inspect where the current config came from.- Added reactive
EconomyWalletwith atomic earn/spend and transaction idempotency. - Added
GameSessionControllerfor typed game session state, nested pause reasons and lifecycle bridging. - Added typed
SdkResultsuccess/failure contracts for public service APIs. - Added
SaveMigrationRegistryfor validated multi-hop save upgrades with copy-on-write migration. - Added
AsyncActionGuardfor keyed single-flight and ordered exclusive actions. - Added the GetX lifecycle coordinator for ordered, isolated background/resume hooks with storage flush and audio handling.
- Added a deterministic consumer contract-test fixture and bootstrap verification harness through the public package entrypoint.
- Core services:
ClampedClock-backed cheat-proof offline earnings (OfflineProgressionService),VersionedJsonStore,PerformanceTierService,DailyLoginService,EnergyService,CrashReporterseam, plus reduced-motion support (NeonTheme.reducedMotion) applied across every animated widget in the kit. - Widget kit grown from 21 to 40 widgets: new progress/reward widgets (
CountdownChip,PaginatedDotsIndicator,CoinFlyOverlay,DailyLoginCalendarWidget,EnergyBar), new layout/card widgets (RibbonBadge,ShopItemCard,VictoryCardTemplate,LeaderboardList), new feedback/overlay widgets (ConfettiOverlay,FloatingComboText,NetworkStatusBanner,ShimmerPlaceholder,SpotlightOverlay), a game-specificLevelSelectGrid, and a game-feel/juice set (SquashStretch,ScreenShake,ComboHeatBackground). - New Flame starter template (
RoyGame) andFlameTrackedOverlayfor pinning Flutter widgets to in-game world positions. - Notable bug fixes: Flame camera-anchor/world-origin mismatch (BUG topLeft anchor),
ClampedClockrewind resistance extended toVersionedJsonStore, several real-device narrow-widthRenderFlexoverflows (CurrencyCounter,LevelSelectGrid),CommonButtonheight rebalanced,RewardPopup's lazily-initialized burst controller.
0.1.0 #
- Initial release: core services (storage, i18n, audio, haptics, local reminders, theme tokens) and a 21-widget casual-game UI kit (buttons, overlays, progress/reward, layout & cards).
