Skip to main content

roy_casual_kit 0.6.3 copy "roy_casual_kit: ^0.6.3" to clipboard
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 WorkloadPerformanceBudget in core/utils/performance_budget.dart to evaluate integration test frame timings against configurable p95, slow-frame percentage, and minimum frame thresholds with actionable failure diagnostics, plus extractMetrics for baseline integration.
  • Added localized tooltips and semantics to GameDemoScreen's icon-only pause and info FloatingActionButtons (game_demo_pause_tooltip and game_demo_info_tooltip in both en and vi locales).
  • Hardened MonetizationScreen demo to verify EconomyWallet.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 in monetization_screen_test.dart, key parity in app_translations_test.dart, and 21 tests in game_demo_screen_test.dart pass.
  • 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 ExperimentVariantAssignmentCard to hide and reject debug variant overrides in release builds, matching ExperimentBucketingService.debugSetVariantOverride.
  • Restricted OnboardingFlowQueueCard mark-seen action to caller-designated nextEligibleFlow; 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 local export barrels (including show/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) and CheckpointResumeCard (confirm before discarding a checkpoint).
  • Added content-delivery widgets ContentPackStatusCard, ScenePreloadStatusPanel and ContentRevisionHistoryPanel. 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 example RemoteContentPack.rollbackToChecksum) and refreshes its content.
  • Added offline-delivery widgets OutboxQueueStatusCard and OutboxConflictSummaryPanel. 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, for PlayerDataRightsService), PrestigeReadinessCard (progress toward the threshold; the button is enabled only when ready, and the caller should confirm, for example with PrestigeConfirmationCard.show, before calling PrestigeService.prestige) and CooldownActionButton (disabled while a PersistentCooldownService cooldown 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 example ConsentStateService.grant/deny), SyncStatusCard (idle/syncing/synced/failed state with a retry button for achievement or leaderboard sync; the caller maps its SdkResult to the status) and BatterySaverIndicator (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) and DeepLinkResultCard (generic outcome per status; it never renders the link, its parameters or an exception, so the caller maps DeepLinkHandleResult to 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) and GuardrailStatusPanel (read-only caller-supplied rollout rows, without new read APIs or actions on ShadowActivationController).
  • Added MemoryWatchdogPanel (bounded caller-fed debug diagnostics; detector only), AnnouncementPreviewCard (shows caller-resolved localized text and invokes a caller preview callback) and RewardedAdResultCard (caller-owned seam state; it never grants a reward, which the caller grants exactly once after showRewardedAd returns true).
  • Added AnalyticsQueueStatusCard (caller-supplied queue snapshot: counts and generic status only, never event names or payloads) and SaveRestorePreviewPanel (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), and ConnectivityStatusCard (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 sets applied only 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) and RewardAuditTrailPanel (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) and ExperimentVariantAssignmentCard (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.dart and 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.verifyAchievementSyncSeam with 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) and AchievementSyncCoordinator, a platform-neutral way to sync AchievementService progress with Play Games, Game Center or your own backend. The package ships no backend client; register your adapter with Get.put<AchievementSyncSeam>(adapter, permanent: true).
  • AchievementSyncCoordinator.sync() pulls, merges per achievement with max(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 retryable SdkFailure(network); a failed local save returns SdkFailure(storage). Resetting or lowering progress is not supported by this merge.
  • Added AchievementService.mergeProgressDurably(remote) (raises progress, fires onUnlock for newly completed achievements, awaits the save) and AchievementService.progressSnapshot (read-only copy). Purely additive: no existing signature changed.
  • Fixed AchievementService trimming 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 throws ArgumentError. Save format is unchanged.
  • Added an AchievementSyncSeam tile to the example cookbook. Added the workflow_dispatch trigger to CI so the gated jobs can be run on demand.

0.4.5 #

  • Fixed InventoryService leaving its in-memory state changed when persisting failed. grant, consume, setEquipped and moveSlot changed the slots, the next slot id and the transaction ledger before writing, and the storage error was thrown without undoing that. A retry of grant with the same transactionId then returned SdkSuccess although nothing had been saved, and a failed consume kept 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 (SdkResult equality and hash, the level-up haptic type, ReactiveEnergyBar service swap, daily quest read failures). No other library code changed.

0.4.4 #

  • Fixed InventoryService.consume checking 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 with SdkErrorKind.validation when the total exceeds what is owned.
  • Fixed PlayerProgressionService.grantXp leaving 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 same transactionId returned SdkSuccess without 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, PerformanceTierService and 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_NOTIFICATIONS and VIBRATE, so the app does not declare them. ScheduledNotificationReceiver is 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.getStringList throwing when the stored JSON list held non-string elements (e.g. [1,2]): the lazy cast<String>() escaped its try/catch and 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 to roy_casual_kit: ^0.4.1. CI gained a consumer-smoke job that builds and runs a generated consumer app on an emulator.

0.4.1 #

  • Fixed VersionedJsonStore.syncWith(..., onConflict:) returning SdkSuccess when the conflict resolution's storage write or cloud upload failed, silently losing the save. It now returns SdkFailure (SdkErrorKind.storage or SdkErrorKind.network). A handler that throws still falls back to last-write-wins.
  • Fixed Baloo2 not loading in consumer apps: NeonTheme.fontFamily and the fitFontSizeForLongestWord default now use the package-qualified family packages/roy_casual_kit/Baloo2.

0.4.0 #

  • Note: throttled(window:) now throws ArgumentError for a negative window (previously it silently disabled throttling), and RetryPolicy.delayBeforeAttempt now throws ArgumentError for attemptNumber < 1, a randomValue outside [0, 1], or a policy with invalid maxAttempts/jitterFraction, in release builds too (ENH-85). attemptNumber: 0 previously returned Duration.zero.
  • Fixed fitFontSizeForLongestWord returning the first fitting candidate instead of the largest fitting one when passed an unsorted candidates list (BUG-101).
  • Note: ObjectPool(maxCapacity:) and fitFontSizeForLongestWord(candidates:) runtime validations are now backed by ArgumentError (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 by GameEventBus for 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.withHistory and GameSessionController.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/setSfxVolume with restore-on-focus/ducking behavior (ENH-95).
  • Added direct unit/widget coverage for AdRewardSeam, RuntimeFlags, debug_log, the common_widgets barrel, the BUG-92 PauseOverlay edge case, and RoyCasualKitContractTestKit (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.dart to scan multiple asset roots via --asset-roots so example/'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/vi translation 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 EnergyBar reactive to its underlying stream (BUG-95).
  • Fixed GameEventBus subscriptions leaking past widget dispose in the Game Demo, and AchievementUnlockListener only ever subscribing once instead of re-resolving on didUpdateWidget (BUG-93).
  • Surfaced storage failures from AchievementService instead of silently dropping them, and made EconomyWallet multi-currency updates atomic via a batch transaction so PrestigeService can no longer partially apply (BUG-90).
  • Fixed RemoteSchemaDef accepting reserved Dart keywords as identifiers, a validated schema still holding a live (mutable) reference to the caller's field list, and BouncingOrb'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 original RemoteContentPack(...) constructor remains unchanged and cache-free for full source compatibility.
  • Added reward-plan dry-run preview/execution (FEAT-96): RewardTransactionPipeline.withInventory, preview, and executePlan unify currency (EconomyWallet) and item (InventoryService) rewards into one immutable plan; preview is deterministic and write-free, execute rejects stale state with SdkErrorKind.conflict, and item failures reuse the existing persisted pending/partial/committed + resumePending flow instead of attempting a fragile compensating rollback. Added InventoryService.previewGrant, RewardTransactionRecord.withItems, and grantWithItems; all original constructor/method signatures remain unchanged (API-gate additive only).
  • Added VersionedJsonStore.syncWithResult (BUG-84), a non-breaking typed-result companion to the existing syncWith: cloud download/upload transport failures now return SdkFailure(kind: network) instead of escaping as unhandled exceptions; the original Future<void> syncWith API remains source-compatible and degrades to a safe no-op on network failure.
  • Fixed a race between StorageService.flush() and a direct setInt/setBool/setDouble/setString write 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's restore() 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-registered AudioManager/WakeLockService behind when that module's init() throws (BUG-86) — a retried initialize() now cleanly re-attempts instead of finding a broken instance already "registered".
  • Fixed SaveSlotManager.deleteSlot removing 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 its SdkResult contract on a storage failure (BUG-88) — a failed persist now rolls the in-memory record back to its last known-good snapshot and returns SdkFailure(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 same drain() 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, celebratory ConfettiOverlay, and unlock banner (Option B).
  • Injected nowMs testable clock seam into RewardTransactionPipeline.grant() (BUG-81): replaces hard-coded DateTime.now() with an injectable int Function()? nowMs param, 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, reused ConfettiOverlay painter allocation state to reduce per-frame GC churn, and added optional RoyLifecycleCoordinator(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 invalid capacity, transactionCapacity, failuresToGoOffline, or maxQueueSize (<= 0) via ArgumentError.value, preventing ledger truncation and replay vulnerabilities.
  • Added release-mode runtime policy validation in RetryExecutor.run without breaking const RetryPolicy constructors, returning typed SdkFailure(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 ConsentBanner with 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 ConnectivityCoordinator asynchronous continuations against post-close invocations, eliminating Bad state: Cannot add new events after calling close and 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) — wraps wakelock_plus with a persisted on/off preference (StorageKeys.wakeLockEnabled, default true) instead of unconditionally forcing the screen awake, registered via a new RoyCasualKitModule.wakeLock bootstrap module. Same shape as AudioManager.muted: an RxBool enabled, .maybe null-safe accessor, setEnabled/toggle. example/: replaced the hardcoded WakelockPlus.enable() in main.dart with the module + a "Keep Screen On" toggle in SettingsScreen (same CandyToggleSwitch pattern as the sound/dark-mode toggles); dropped the now-redundant direct wakelock_plus dependency from example/pubspec.yaml (comes transitively through the root package). Tests: test/core/wake_lock_service_test.dart (9 cases) + 2 new example/test/settings_screen_test.dart cases (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 every lib/core/ service (grouped by the same 9 categories), plus a "Utilities" category for lib/core/utils/; fixed 2 stale class names found while writing it (AppVersionGate → AppVersionGateController, InAppReviewHelper → the actual top-level maybeRequestReview function) and one wrong method name (DisasterRecoverySaveExport.signExport → .sign). Added example/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 their Fake* reference adapters, PluginAdapterConformanceSuite, AppVersionGateController, GameTimeController, maybeRequestReview, MemoryWatchdog, AssetPreloadCoordinator, RoyLifecycleCoordinator.registerHook, HapticChoreographer), each tile firing one real call and showing its real result in a ToastBanner. Added example/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 a ToastBanner-ticker-teardown flake that only reproduced under the full test suite's concurrent isolates — 3 asset-loading tiles in the last file are skip: true with the bisection written up in-file, same precedent as the existing ConfettiOverlay/Page Dots skip: true tests; 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 in adb logcat). Added CI quality-gate job (.github/workflows/ci.yml) wiring accessibility_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 via dorny/paths-filter to only run when lib/**/tool/** change, sharing the existing concurrency: cancel-in-progress group 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, runs tool/performance_budget_check.dart snapshot --device=<id>, regenerates the README's "## Performance" table via the new tool/update_readme_benchmarks.dart (+ test/tool/update_readme_benchmarks_test.dart), and opens a PR with the refreshed numbers rather than pushing to main directly.

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 to raw.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's description and 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). Added example/README.md (pub.dev's "Example" tab prefers it over dumping raw source) pointing readers to which example file covers what, rather than shrinking example/lib/main.dart into a toy snippet — it stays a real, full bootstrap on purpose. Added screenshots: to pubspec.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) plus tool/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 upgrading roy_casual_kit can 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. buildMigrationRegistry wires the schema's version history into the existing SaveMigrationRegistry (FEAT-37) rather than reinventing chain validation; verifySignedFixture gates a fixture through the exact signature+version checks RemoteContentPack already applies at runtime (save_integrity.dart's verifyAndStrip, reject-future-version) before the CLI will generate anything from it. Added example/remote_schemas/shop_catalog.schema.json + its compiled example/lib/generated/shop_catalog_content.g.dart as a real consumer integration, cross-checked in example/test/shop_catalog_content_cross_check_test.dart (asset-fallback path via RemoteContentPack and the generated model's own fromJson produce the identical object for the same fixture).
  • Added PrivacyAwareAnalyticsSampler (FEAT-83, lib/core/privacy_aware_analytics_sampler.dart): an AnalyticsProvider decorator combining consent gating (same default-deny rule ConsentGatedAnalyticsProvider already enforces), deterministic per-session sampling (an event name is hashed with the session's sessionId via the existing fnv1aHash, 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 existing SdkEventSchemaRegistry (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. auditSnapshot exposes cumulative forwarded/dropped-by-AnalyticsDropReason counts for a health/debug surface.
  • Added PerformanceBudgetMetric/BudgetPolicy/checkPerformanceBudgets (FEAT-80, lib/core/utils/performance_budget.dart, no Flutter dependency) plus tool/performance_budget_check.dart (check/snapshot subcommands, same split as tool/api_compatibility.dart): a CI performance-budget gate honest about where each number came from via MeasurementSource (hostHeadless vs realDevice) — a policy requiring realDevice fails outright (wrongSource) rather than silently accepting a host-measured number as device proof. hostHeadless metrics are re-measured fresh every check run from this repo's own tool/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. realDevice metrics can't be reproduced headlessly in CI: snapshot --device=<id> runs example/integration_test/app_boot_test.dart on a real, currently-connected device and records its real wall-clock boot time; check (no --device) falls back to whatever realDevice value is already committed in tool/performance_budget_baseline.json, gated by each policy's maxAgeMs (a stale device reading is a staleMeasurement failure, not a silent pass, forcing a periodic real re-run). regressionTolerancePercent flags a value that got meaningfully worse than the committed baseline even while still inside the hard budget. Seeded tool/performance_budget_baseline.json from 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 to ActivateIntent by Flutter's own default shortcuts) now trigger onTap when focused; a focus ring only ever renders when FocusManager.instance.highlightMode is FocusHighlightMode.traditional, so a touch-only player never sees any visual change. A widget with onTap: null gets no Focus/Actions wiring at all (unchanged from before, still not focusable). Also added FocusTrapScope: wraps an in-tree modal's content (this package's overlays are never real routes, see the Dialog pattern in CLAUDE.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 into PauseOverlay, 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 composing SaveSlotManager (slot metadata/key prefixes), StorageService.exportWithPrefix/importWithPrefix, and save_integrity.dart's HMAC sign/verify. buildExport rejects 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. previewRestore verifies the signature, validates schemaVersion/shape, and returns a read-only RestorePreview (slot names, timestamps, key counts) WITHOUT ever writing to storage, so a corrupt/tampered backup can never mutate a local save. applyRestore restores slot-by-slot in order, stopping at the first failure and logging every step to a recoveryLog — crash-safe because importWithPrefix's own underlying importAll (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 bypassed AppTranslations/.tr stays 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 synthetic qps/PLOC locale (same convention Android's own pseudolocalization tooling uses) — never in AppTranslations.supported. example/'s SettingsScreen wires a debug-only (kDebugMode-gated) "Pseudo-locale (QA)" toggle demonstrating it live via Get.addTranslations/Get.updateLocale.
  • Added parsePubspecLock/SbomDocument/classifyLicenseText/auditDependencies (FEAT-78, lib/core/utils/dependency_sbom.dart, no Flutter dependency) plus tool/dependency_sbom_check.dart (a genuinely headless dart run CI check): parses pubspec.lock into a reproducible (name-sorted) SBOM, classifies each hosted dependency's bundled LICENSE file (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 an owner and an expiresAtMs — once expired, the issue reappears automatically, no permanent unowned exceptions. Running this against the package's own pubspec.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 (dbus on 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 registered EventSchema before it ever reaches a real AnalyticsProvider (FEAT-03). An unregistered event name, a missing required param, or a required param's wrong type all reject the whole event (sanitizedParams empty); an optional param's wrong type drops just that field; a pii: true param is ALWAYS redacted regardless of anything else (a schema can't declare a param both required and pii — 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 own unknownFieldPolicy. migrate upgrades an older client's raw params to the current schema shape before validation (same convention VersionedJsonStore.migrate already uses). SchemaValidatedAnalyticsProvider is a decorator (composes with ConsentGatedAnalyticsProvider the same way that one already wraps a real provider) that never lets a throwing inner provider crash gameplay — the error goes to CrashReporter.maybe instead. auditLog is a bounded history of every validation decision; eventSchemaAuditHealthCollector(registry) is an opt-in SdkHealthReport (FEAT-39) section reporting recent rejection activity.
  • Added scanAccessibility/AuditRule/AuditViolation/AuditBaseline (FEAT-76, lib/core/utils/accessibility_audit.dart, no Flutter dependency) plus tool/accessibility_audit_check.dart (a genuinely headless dart run CI check, same pattern as tool/asset_license_check.dart): source-text-scans a widget tree for 3 statically-checkable accessibility rules — reduced-motion coverage (an AnimationController with no reducedMotion reference, error), tap-target semantics coverage (a GestureDetector/InkWell with no Semantics/semanticLabel/ExcludeSemantics, warning), and RTL directional safety (a literal left:/right: on EdgeInsets.only/Positioned instead of start:/end:, warning). Deliberately does NOT attempt contrast or text-scale coverage — both need real rendered pixels/layout, already covered by theme_contrast_validator.dart (FEAT-68) and the golden matrix runner (FEAT-69) respectively. tool/accessibility_audit_baseline.json documents 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's reducedMotion mention 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 of RemoteConfigService (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, false if the feature isn't in that map). states is an RxMap, so a screen already open when ops flips a switch remotely reacts via Obx without 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. auditLog is a bounded (200) history of every decision made, each carrying its reason/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 calls track() when it creates one and release() when it properly disposes it, and orphans() reports anything still tracked (a leak candidate). Ships zero overhead in release the same way dlog() does: every method short-circuits on !kDebugMode before 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 is NeonBg's permanent background-animation Ticker (see the CLAUDE.md testing gotcha). memoryWatchdogHealthCollector() is an opt-in SdkHealthReport (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) plus tool/asset_license_check.dart (a dart run-able CI check, same headless pattern as tool/economy_sim.dart/tool/api_compatibility.dart): walks the package's runtime asset roots (asset/, shaders/) and cross-checks every file against asset/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-redistribution by default, or distributable: false) — never crashes on a missing/malformed manifest, reports every runtime asset as missing instead. asset/LICENSES.json now documents this package's actual 4 shipped assets (bkg.ogg, Baloo2.ttf, neon_glow.frag, aurora_bg.frag). Not wired into .github/workflows/ci.yml by this task (same reasoning as FEAT-68/69 — see this task's Quyết định).
  • Added DiagnosticsExportBundle/DiagnosticsBundleView (FEAT-70): packages an SdkHealthReport snapshot (FEAT-39), a ReplayCapsule (IDEA-42), a redacted config slice, and recent log lines into one {schemaVersion, generatedAtMs, appVersion, sections, errors, truncated} JSON support artifact — signed via save_integrity.dart's existing signExport/verifyAndStrip (same convention ReplayCapsule.exportSigned already uses). Default-deny redaction for the config section, matching HealthCollectorSpec.allowedKeys's existing convention: passing config without configAllowedKeys produces an empty (not full) section — a caller must opt keys in by name. Deliberately never accepts a raw StorageService.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 exceeds maxBytes (default 200 KB), sections are dropped least-essential-first (logs, then replay, then config; health kept as long as possible) until it fits, every drop recorded in errors and truncated set — a partial failure never loses the rest of the bundle. DiagnosticsBundleView.fromJson/fromSignedJson parse a bundle read-only — parsing never touches StorageService or any registered service, unlike VersionedJsonStore import (which is meant to restore state).
  • Added a Golden Matrix Runner (FEAT-69, test/support/golden_matrix.dart — test-only, not exported: it depends on flutter_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, vi locale, RTL, 2 text scales, reduced motion — 7 cases total, not a full cartesian product) and fails on tester.takeException() (overflow/render error) or a broken androidTapTargetGuideline/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 against CommonButton (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's androidTapTargetGuideline check while building it (real payoff of the new tool, not a review finding). Fixed with constraints: BoxConstraints(minHeight: 48); ShopItemCard's golden fixtures (the only other golden-tested widget embedding CommonButton) regenerated to match.
  • Added validateNeonThemeContrast/ContrastCheck/ContrastIssue/ThemeContrastConfig (FEAT-68): a WCAG contrast validator for NeonTheme — checks ink/inkSoft on card/cardAlt (normal-text threshold, default AA 4.5:1), muted on card/cardAlt (a sanity-only "not literally invisible" check, since WCAG exempts disabled controls from a real contrast bar), lockedBorder on lockedFill (UI-component threshold, default 3.0:1), and every pair of NeonTheme.gemColors (an RGB-distance heuristic, not a real CVD simulation) across both dark states and both colorBlindSafe states — restoring both flags afterward, so calling it never leaves the live theme mutated. validateNeonThemeContrastPairs is the underlying pure function (plain ContrastCheck colors in, no NeonTheme dependency), 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's doc/task/done/FEAT-68-theme-contrast-validator.md): inkSoft on light-theme card/cardAlt reads 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 — and lockedBorder/lockedFill reads at 1.43:1 regardless of dark, since neither token has a separate dark variant. No CLI tool ships for this one: unlike tool/economy_sim.dart/tool/api_compatibility.dart, a real check needs NeonTheme's actual Color values, and NeonTheme transitively imports dart:ui — unavailable to a plain dart run process 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 in flutter test instead, which is already a CI step.
  • Added InventoryGrid (FEAT-56): a data-driven grid over an InventorySnapshot (FEAT-44) — itemBuilder is 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 call InventoryService.consume/setEquipped/moveSlot itself. Cells beyond snapshot.slots.length render as empty up to an optional unlockedCapacity (default: no locked cells at all), then locked beyond that — InventorySnapshot itself has no "locked" concept, this is a presentation-only opt-in. Built on GridView.custom with an explicit SliverChildBuilderDelegate.findChildIndexCallback keyed by InventorySlot.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; without findChildIndexCallback, 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 own slotId — caught by a widget test using a stateful item builder before this shipped, not by inspection. Supports drag-to-reorder via onReorder(fromSlotId, toSlotId) (also opt-in, disabled by default) built on Draggable/DragTarget.
  • Added LevelUpOverlay/LevelUpOverlayController/LevelUpCelebration (FEAT-54): a data-driven level-up celebration overlay — the caller supplies LevelUpEvents already obtained from PlayerProgressionService.grantXp (FEAT-43); this widget only presents them, it never calls grantXp/RewardTransactionPipeline itself. Sequences xpFill → levelPop → rewardReveal per queued level, in ascending order, then either moves to the next queued level or back to idle. LevelUpOverlayController is a pure Dart, token-guarded state machine (same shape as SceneTransitionController, FEAT-58) — every show() mints a token, and only that token's continuation may mutate state, so a superseded show() silently drops its own late completion instead of double-firing onComplete. skip() jumps straight to idle and fires onComplete (once, guarded the same way) instead of waiting out the remaining phases — implemented with a real cancellable Timer per phase (not Future.any([Future.delayed(...), ...]), which never cancels its losing branch and would otherwise leave a real pending timer alive after an early skip). Confetti (reusing ConfettiOverlay) and haptic feedback are both optional and automatically skipped under reduced motion.
  • Added AdaptiveGameHud/HudSlot/HudBreakpoint (FEAT-52): a HUD-content placement widget that reserves topStart/topCenter/topEnd/bottom/side/overlay slots without a caller hand-rolling safe-area, notch, keyboard-inset, RTL, and breakpoint handling itself. Every edge slot is padded by MediaQueryData.padding directly (never wrapped in SafeArea, which would apply the same inset to every slot even when only one edge needs it); bottom additionally clears viewInsets.bottom so an on-screen keyboard never covers it. topStart/topEnd/side use Flutter's own PositionedDirectional, so RTL swaps sides automatically with no custom logic. HudSlot.side hides entirely below compactBreakpointWidth (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 resolved HudBreakpoint via AdaptiveGameHud.breakpointOf. An optional flameViewportBounds (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 wraps slots in its own Obx/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() throws StateError on a double-release or a foreign object this pool never handed out — identity, not ==, decides what's currently active, so a T overriding equality can't confuse the check. maxCapacity bounds what release() retains (prewarm()/normal reuse), never what acquire() can hand out — a burst past capacity still succeeds, just without retaining the surplus past its next release. PooledComponent is a Flame Component mixin that calls back into its pool automatically from onRemove(), via a plain void Function() callback (attachToPool) rather than holding a typed ObjectPool<T> reference, sidestepping Dart's invariant generics entirely. tool/object_pool_benchmark.dart (headless, no Flutter/Flame dependency, cross-checked in test/tool/object_pool_benchmark_test.dart) runs a representative particle-burst scenario through the real ObjectPool class 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 on RetryExecutor (FEAT-35) for transient-failure backoff and, when a ConnectivityCoordinator (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 from enqueue() 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 next drain() — it always retries with the same OutboxItem.idempotencyKey, so nothing is ever silently lost; avoiding a duplicate effect on the remote side is the injected OutboxUploader's own responsibility (a real backend keys on that same id to dedupe a retried request it already applied). enqueue() only auto-drains when a ConnectivityCoordinator confirms it's online right now — without one, back-to-back enqueue() calls would otherwise race their own fire-and-forget drain attempts against each other and break the priority ordering a single drain() call guarantees. A SyncConflict response is handled per ConflictPolicy: reject discards 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); merge calls a required ConflictMerger and re-uploads once more, falling back to manual instead of looping forever if that still conflicts; manual parks the item in manualReviewItems (with the authority's remotePayload attached) until resolveManual picks ManualResolution.keepLocal (re-queues the original payload) or acceptRemote (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 as EconomyWallet (FEAT-31)/RewardTransactionPipeline (FEAT-42) — every mutating call (grant, consume, setEquipped, moveSlot) runs under AsyncActionGuard.runExclusive('inventory', ...), so concurrent calls serialize deterministically instead of racing, and a repeated transactionId on grant/consume (even across a restart) is a no-op. A grant/consume call takes a list of InventoryLines and applies them atomically against a scratch copy first — if any single line would exceed capacity (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. grant fills existing under-maxStack stacks of the same item before opening a new slot. ItemDefinition is entirely caller-supplied (no concrete item list baked into the kit) — an unknown item id in a grant line 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 way EconomyWallet does. setEquipped/moveSlot are naturally idempotent (no transaction id needed) and target a slot's stable slotId, 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 as EconomyWallet (FEAT-31) and RewardTransactionPipeline (FEAT-42) — grantXp(amount, transactionId) runs under an AsyncActionGuard.runExclusive key, so concurrent grants serialize instead of racing, and a repeated transactionId (even across a restart) is a no-op that returns the current snapshot rather than double-counting. level/xpIntoLevel/xpToNextLevel are never persisted directly — only lifetime totalXpEarned is, and the rest is always re-derived from it against the curve (pure levelForTotalXp/xpIntoLevelForTotalXp), so they can never drift out of sync with a curve change. validateLevelCurve rejects a broken LevelDefinition list (duplicate/missing level number, a non-final level with xpToNext <= 0, a decreasing curve, or cumulative XP overflowing the supported range) at construction time — a bad config throws ArgumentError immediately rather than producing silently-wrong level math later; the curve's last entry must have xpToNext == 0, which is what marks the max level. A grantXp call that crosses several level thresholds at once (a big XP grant, an offline-earnings catch-up) grants each newly-reached level's unlockRewardLines exactly once through an injected RewardTransactionPipeline (deterministic 'level_up:$level' transaction id) and fires onLevelUp once per level in ascending order; XP past the curve's max level is still tracked in totalXpEarned for analytics honesty, but level itself 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 as EconomyWallet'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 — same NeonDialog-style in-tree-overlay pattern (works over a full-screen Flame GameWidget, where Navigator/showDialog can't). The controller is pure Dart (no Animation/BuildContext), unit-tested as a state machine before any animation code was written: every run() mints a token, and only that token's continuation may mutate phase/progress/lastError — a still-in-flight run() superseded by a newer one (rapid re-navigation), or one still resolving after cancel()/dispose(), silently drops its own late completion instead of stomping the current state or leaving a stuck pointer-blocking barrier. progress only reflects the active transition. On failure, the overlay shows RetryErrorState.fromSdkFailure wired to an optional onRetry; the underlying scene is ExcludeSemantics+IgnorePointer-guarded for the whole time the transition isn't idle, so a stray tap/screen-reader focus can never reach the scene being replaced, and the barrier releases the instant phase returns to idle. Respects MediaQuery.disableAnimations (or an explicit reducedMotion override) by collapsing the cover/reveal fade to zero duration instead of animating. runWithAssetPreload wires the controller directly to a real AssetPreloadCoordinator (FEAT-47) + GameSessionController (FEAT-41): preload progress feeds progress, and a successful preload calls markReady()+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 a loader/unloader function pair that dispatches on AssetManifestItem.kind. preload() validates the manifest up front (a dependsOn referencing a missing id, or a dependency cycle, both return a typed SdkFailure(kind: validation) before any loader call runs, via DFS 3-color cycle detection). Items load in dependency-ordered waves, each wave bounded by maxConcurrent in-flight loader calls. progress (0.0–1.0, exposed as a RxDouble) 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, so progress can never falsely reach 1.0 while a required asset is missing — a caller must check the returned SdkResult, not just watch for progress == 1.0. An item id already loaded by an earlier preload() call is never loaded twice — the coordinator tracks a reference count per id, unloadScene() decrements it for the last-preloaded manifest and only calls unloader once 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-runs preload() 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 a false result 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-call timeout, 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/clear must leave no residual readable value, not just "not throw". Ships FakeCloudSaveProvider/FakeCrashReporter/FakePurchaseSeam (in-memory reference implementations, alongside the existing FakeSecureStorageAdapter/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 from kIsWeb/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. withFallback gives 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's isWeb/targetPlatform params are injectable, so its device-matrix test fixtures every platform combination without needing a real device per platform. Demoed in WidgetShowcaseScreen, including a "fire haptic with fallback" button.
  • Fixed CommonButton pills having zero horizontal padding — long labels could visually touch/crowd the pill's rounded edges instead of having proper inset. Added horizontal: NeonTheme.s16 alongside 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 — sequence increments 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 injectable Stopwatch (real elapsed time, not wall clock — a duration measurement doesn't care about clock anomalies) and excludes backgrounded time entirely. A resume within sessionTimeout (default 30 min) of backgrounding continues the same session; a resume after longer starts a brand new one (new sessionId, bumped sequence, foreground duration reset) — the standard mobile-analytics "session timeout" convention. analyticsContext() returns {} unless ConsentCategory.analytics is currently granted on the registered ConsentStateService (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 in WidgetShowcaseScreen.
  • 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 unparseable minimumVersion/recommendedVersion) degrades to GateDecision.ok rather than ever force-blocking a user for a corrupt config; only a plain maintenanceActive bool (can't be "invalid") can force/maintenance-gate. compareAppVersions documents its own semver policy: prerelease sorts before the same core release (1.2.3-beta < 1.2.3), build metadata (+001) is ignored entirely. AppVersionGateController reads straight from RemoteConfigService rather than keeping its own cache — that service already gives "asset default, remote merges in, remote fail/corrupt keeps last-good" for free (ENH-58). Ships AppVersionGateOverlay: force/maintenance block the back button (PopScope(canPop: false)) and aren't dismissible; soft update is dismissible and reports back via onSoftDismiss so the caller can persist the cooldown; a failed injected launchStore call shows an inline error and a "Retry" button instead of looping. Demoed in WidgetShowcaseScreen.
  • Added DeepLinkCommandRouter/DeepLinkRoute/DeepLinkCommand/parseDeepLink (FEAT-60): parses deep links into typed commands against an allowlist of routes (scheme/host/path shape — a :name path 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) into handleUri. parseDeepLink is 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 before markReady() is queued (never dropped) and drains exactly once when the app signals it's ready; a duplicate call for the same URI within dedupeCooldown is ignored, deterministically even for 2 concurrent calls (the cooldown is stamped before any handler awaits). Every handler registered for a command type runs (highest registerHandler priority first) even if an earlier one throws — captured per-entry in a diagnostic DeepLinkHandlerResult, never rethrown, and never affecting a different command's dispatch. Demoed in WidgetShowcaseScreen and wired into the example app's real AndroidManifest.xml intent-filter (roycasualkit://open/...) via app_links.
  • Added ConnectivityCoordinator/ConnectivitySignal/ConnectivityState/QueuedTask (FEAT-62): a reactive Internet-reachability coordinator with a bounded, idempotent retry queue. ConnectivityState.online is reported only once an injected probe (e.g. a HEAD request to a known-good endpoint) actually succeeds — a ConnectivitySignal reporting 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 to ConnectivityState.degraded rather than straight to offline (hysteresis) — only failuresToGoOffline consecutive failures declare it truly offline, so flapping connectivity can't spam the UI. enqueued tasks are capped at maxQueueSize (evicts the lowest-priority entry once full), keyed by idempotencyKey (a duplicate replaces, never duplicates), and drain automatically once back online — highest priority first, through an injectable RetryExecutor/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. connectedStream bridges directly into NetworkStatusBanner.stream. Timers use the same injectable createTimer seam as CheckpointCoordinator/HapticChoreographer, and onClose() cancels every timer/subscription — no leaks. Demoed in WidgetShowcaseScreen alongside a FakeConnectivitySignal test double.
  • Added ConsentStateService/ConsentCategory/ConsentStatus (FEAT-61): an SSOT for privacy consent gating analytics and personalization (no ads category — this kit ships no ads mediation seam, a deliberate product decision from FEAT-02's rejection). Default-deny: a category never explicitly decided reads as ConsentStatus.unknown, never granted, and a corrupted or hand-edited persisted entry is dropped rather than coerced, so it also reads as unknown — never an accidental early unlock. policyVersion support: a category persisted under an older policy version reads back as unknown regardless of its prior decision, forcing a fresh review after a privacy-policy bump. grant/deny/reset persist immediately and take effect immediately within the same instance. Ships ConsentGatedAnalyticsProvider, a decorator over any real AnalyticsProvider that only forwards logEvent while analytics consent is granted — a consuming app registers it in place of its real provider so every call site automatically respects consent. Demoed in WidgetShowcaseScreen, including gating ExperimentBucketingService'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 from nowMsClamped() against a persisted key -> endMs map, the same anti-cheat clamp EnergyService/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 design EnergyService already uses for its own regen math. start/cancel validate 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. Ships CooldownCountdownChip (presentation/widgets/common/), a thin adapter over CountdownChip that takes plain remaining: Duration data (same convention as EnergyBar/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. Each DeprecatedApi entry 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 after deprecatedInVersion), and a migrationHint. DeprecationRegistry.checkAll(currentVersion) reports each entry as active or pastGrace (removal-version boundary is inclusive); pastGraceOnly filters to just the ones that should already be gone. Ships with tool/deprecation_check.dart, a standalone dart run script that reads the package's own pubspec.yaml version and its own kPackageDeprecations list 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.yml by this change).
  • Added SdkHealthReport/HealthCollectorSpec/defaultHealthCollectors (FEAT-39): a QA/support-facing diagnostic snapshot collector registry, safe to export/share by default. Each HealthCollectorSpec declares its own key allowlist — any key its collect() returns that isn't on that list is silently dropped (default-deny, not default-allow), so a copy-pasted token/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 reporting registered: false (not an error) when that module isn't wired up in the consuming app. Wired into DebugQaOverlay'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. RetryPolicy is pure math (immutable, computed entirely in double microseconds so a huge attempt number clamps to maxDelay instead of overflowing); RetryExecutor runs the actual loop with injectable delayFn/randomFn (same seam-injection convention as HapticChoreographer's createTimer), so tests drive it deterministically without real delays. retryIf lets a caller stop immediately on a non-transient error; isCancelled lets a caller abort mid-loop; the final failure's cause/stackTrace are 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. wrapping flutter_secure_storage) with no Noop* default on purpose, same reasoning as PurchaseSeam — silently no-op'ing a secret write would hide a real integration bug. SecureStorage.read/write/delete/clear add the typed SdkResult contract, key validation, and — critically — a clear typed failure when no adapter is registered rather than ever falling back to plain StorageService/SharedPreferences for something meant to be secret. FakeSecureStorageAdapter is 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. GameClock is a pure, no-Flutter-dependency core (mirrors PerformanceTierService's FrameBudgetTracker split) — advance() clamps a negative or huge (backgrounded-for-minutes) delta, applies scale (rejecting a non-finite/non-positive value without touching the existing scale), and supports an optional fixed-step accumulator for deterministic sub-stepping. GameTimeController bridges it to Flame's update(dt) loop via tick() and to Flutter via the elapsed Rx; when a GameSessionController is 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 of StorageService — 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: true cancels that debounce and flushes immediately. A RoyLifecycleCoordinator hook 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's snapshot() throwing (or a non-JSON-encodable aggregate) aborts the whole flush before anything is written, never a partial/mixed commit.
  • Added AsyncCommonButton (FEAT-50): a CommonButton wrapper that drives itself through idle/loading/success/error around an async onPressed — the state machine BackupRestorePanel hand-rolled per-widget before this existed. Guards double-tap/re-entry, supports an optional timeout, auto-reverts to idle after successDuration/errorDuration, respects NeonTheme.reducedMotion.
  • Added RewardTransactionPipeline/RewardLine/RewardTransactionRecord/RewardSource (FEAT-42): orchestrates a reward grant (ad/IAP/quest/daily-login) across multiple currency lines on top of EconomyWallet'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, and resumePending() 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/grantFromPurchase are thin adapters over the generic grant().
  • Added StorageService.eraseAll()/removeAllWithPrefix(prefix) (IDEA-55/IDEA-56): eraseAll() names the classic "Reset progress" Settings action self-explanatorily instead of relying on the undocumented importAll(const {}) trick; removeAllWithPrefix wipes every key starting with a prefix (e.g. one save slot's namespaced keys) via the same rollback-on-error _replaceAll path, and rejects an empty prefix (which would otherwise silently behave like eraseAll under 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/touchSlot for metadata, activeSlotId/setActiveSlot for which slot is current, and keyFor(slotId, suffix) handing out a namespaced storage key for a caller's OWN VersionedJsonStore (this never defines the player-profile schema itself). deleteSlot removes a slot's metadata and every one of its namespaced keys via StorageService.removeAllWithPrefix — no leftover keys, and it becomes the new activeSlotId: null if it was the active one.
  • Added GameSessionController.maybe (ENH-65): brings it in line with every other service in lib/core/.
  • Added loading to ShopItemCard (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 RemoteContentPack treating a missing schemaVersion as "already current" (skipping migrate entirely) instead of "version 0" like VersionedJsonStore (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 touches BuildContext/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's SeededRandom) into a versioned, optionally-signed ReplayCapsule a 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. findFirstDivergence compares 2 ordered outcome lists (a recorded session vs a fresh replay) and reports the first point they differ, with enough context to debug why. DebugQaOverlay gained a Replay tab (start/stop/export) wired to it.
  • Added SeededRandom/RandomSnapshot/CompiledWeightedTable/SeededRandomService (FEAT-45): a deterministic, cross-platform-stable RNG — SeededRandom implements dart:math's Random (so it's a drop-in source for weightedRandomPick/List.shuffle/anything else expecting a Random), but unlike it exposes its internal state via snapshot()/SeededRandom.fromSnapshot for 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). SeededRandomService derives 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. CompiledWeightedTable pre-validates a loot table once instead of every pick() call. Also extracted fnv1aHash (previously private to ExperimentBucketingService) into its own core/utils/fnv1a.dart util now that a second caller needs it.
  • Added EconomyWallet.maybe/RoyLifecycleCoordinator.maybe (ENH-60): the only 2 GetxServices in lib/core/ that were missing the static .maybe getter every other service already has (same gap shape as ENH-19's CloudSaveProvider fix) — both were added after that sweep.
  • CircularProgressRing/WheelSpinner gained proper Semantics (ENH-59): both are drawn entirely via CustomPaint, and previously exposed nothing to a screen reader (CircularProgressRing's optional label/icon still got their own automatic semantics, but never conveyed "this is a progress indicator" or its percentage). CircularProgressRing now announces "Progress: N%" (overridable via a new optional semanticLabel param) plus a matching value. WheelSpinner now 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.playSfx gained a duck param (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 as AudioManager.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 on muted, 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 to DailyQuestService (IDEA-29), which previously had no widget consuming it. Shows a list of quests (label + progress bar + Claim button), same pure/data-driven convention as LeaderboardList — the caller maps its own DailyQuestService state into QuestViewModels 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), respecting NeonTheme.reducedMotion.
  • Added AchievementService.onUnlock and AchievementUnlockListener (IDEA-43): onUnlock is a broadcast Stream<String> firing exactly once per achievement id the moment incrementProgress pushes it from not-completed to completed (never from register, even if a newly-declared threshold is already retroactively met; never again on further increments). AchievementUnlockListener wraps a subtree and shows a ToastBanner for every unlock — the first widget in the kit to actually consume AchievementService, 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 on RemoteConfigService's asset-fallback-then-fetch pattern, VersionedJsonStore's schema-version migrate, and save_integrity.dart's HMAC signing. load() always resolves from the bundled asset first — never network-gated — then a fetchRemote fetch (if supplied) refreshes current in the background once it resolves. A fetched envelope with a bad/missing signature, a future schemaVersion, or a shape fromJson can't read is rejected silently, keeping whatever content was already applied — never crashes, never applies unverified content. A pack with no contentSecret configured never trusts a signed envelope at all (no key to check it against).
  • DebugQaOverlay gained a Playground tab (IDEA-39) alongside its existing read-only state dump — a QA tester/designer can live-preview a CommonButton with 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, unlike utils/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 new ClockSample (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 legacy StorageKeys.maxMsSeen watermark on first use. Standalone and fully tested — not yet wired into any existing time-gated service (see the task's ## Quyết định for the scope reasoning); an optional TrustedTimeSource seam 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 of fireHaptic — HapticPattern.reward/combo/error presets (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.dart fast-forwards a scripted session pattern (sessions/day, energy spent/session) through EnergyService's regen math and OfflineProgressionService's earnings math, printing the resulting energy/currency curve across simulated days, no Flutter runtime required. Extracted the 2 services' pure formulas into core/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')) into List<TutorialStep> for the existing TutorialSequenceController.start — a designer can now reorder/reword/A-B-test onboarding without a rebuild. A step referencing an unknown targetKey name is skipped, never crashes. TutorialStep gained an optional id; when set, TutorialSequenceController logs tutorial_step_shown/tutorial_step_dismissed through AnalyticsProvider as the sequence advances (an imperative step with no id logs nothing, unchanged from before).
  • Added SeasonEventService (IDEA-34): a repeating, has-a-time-window event/season schedule for CountdownChip. currentWindow(eventId, {length, cooldown}) returns the same SeasonEventWindow (start/end) for repeated calls within one length + cooldown cycle, including across app restarts (anchor persisted per eventId), 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 of PurchaseSeam — grantConsumable/consume for a balance that can never go negative (consume returns false instead of overdrawing), grantPermanent/owns for one-time unlocks. Not a receipt-validation layer — meant to be called by the app's own PurchaseSeam adapter only after it has already verified a purchase.
  • Added ExperimentBucketingService (IDEA-32): stable A/B-test variant assignment on top of RemoteConfigService'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 BackupRestorePanel widget (IDEA-31): puts StorageService.exportAll()/importAll() and save_integrity.dart's HMAC sign/verify on screen. Caller supplies secret, onExport(signedJson) and onImport() -> signedJson? seams (the actual file/QR/share mechanism stays app-specific, same convention as CloudSaveProvider); 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) returns LeaderboardEntry directly (rank + formatted score via fmtNum), ready to feed straight into LeaderboardList with 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 permanent AchievementService and the fixed-calendar DailyLoginService. register(id, targetCount, {period}) declares a quest once; incrementProgress/isCompleted/claim track it per QuestPeriod (daily/weekly), resetting automatically on the ClampedClock-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-only snapshot getter and a source getter (RemoteConfigSource: assetOnly/remoteMerged/remoteFailed) to inspect where the current config came from.
  • Added reactive EconomyWallet with atomic earn/spend and transaction idempotency.
  • Added GameSessionController for typed game session state, nested pause reasons and lifecycle bridging.
  • Added typed SdkResult success/failure contracts for public service APIs.
  • Added SaveMigrationRegistry for validated multi-hop save upgrades with copy-on-write migration.
  • Added AsyncActionGuard for 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, CrashReporter seam, 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-specific LevelSelectGrid, and a game-feel/juice set (SquashStretch, ScreenShake, ComboHeatBackground).
  • New Flame starter template (RoyGame) and FlameTrackedOverlay for pinning Flutter widgets to in-game world positions.
  • Notable bug fixes: Flame camera-anchor/world-origin mismatch (BUG topLeft anchor), ClampedClock rewind resistance extended to VersionedJsonStore, several real-device narrow-width RenderFlex overflows (CurrencyCounter, LevelSelectGrid), CommonButton height 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).
1
likes
140
points
867
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#widget #game #ui #flame #casual-game

License

MIT (license)

Dependencies

crypto, flame, flame_audio, flutter, flutter_local_notifications, get, intl, qr_flutter, share_plus, shared_preferences, timezone, wakelock_plus

More

Packages that depend on roy_casual_kit