From d406f41acce3cbc42317a184755bae16b29be815 Mon Sep 17 00:00:00 2001 From: Savya Bikram Shah Date: Sun, 7 Jun 2026 15:53:16 +0545 Subject: [PATCH] Bus and crash fixes UX improvements rewarded ads added --- .../Core/Contracts/Features/SaveGate.meta | 8 + .../Features/SaveGate/IRewardedSaveGate.cs | 23 + .../SaveGate/IRewardedSaveGate.cs.meta | 2 + .../Features/ArtBook/UI/ArtbookPresenter.cs | 10 +- .../Installers/CaptureFeatureModule.cs | 1 + .../Capture/UI/CaptureButtonPresenter.cs | 19 +- .../Coloring/UI/CompletionAnimationView.cs | 6 + Assets/Darkmatter/Code/Features/SaveGate.meta | 8 + .../SaveGate/Features.SaveGate.asmdef | 20 + .../SaveGate/Features.SaveGate.asmdef.meta | 7 + .../Code/Features/SaveGate/Installers.meta | 8 + .../SaveGate/Installers/SaveGateModule.cs | 31 + .../Installers/SaveGateModule.cs.meta | 2 + .../Code/Features/SaveGate/SETUP.md | 61 + .../Code/Features/SaveGate/SETUP.md.meta | 7 + .../Code/Features/SaveGate/Systems.meta | 8 + .../SaveGate/Systems/RewardedSaveGate.cs | 145 ++ .../SaveGate/Systems/RewardedSaveGate.cs.meta | 2 + .../SaveGate/Systems/SaveGateConfig.cs | 18 + .../SaveGate/Systems/SaveGateConfig.cs.meta | 2 + .../Darkmatter/Code/Features/SaveGate/UI.meta | 8 + .../SaveGate/UI/SaveGalleryOverlayView.cs | 77 + .../UI/SaveGalleryOverlayView.cs.meta | 2 + .../Tutorial/Systems/TutorialDirector.cs | 20 +- .../Content/Colorbook UI/Prefabs/.DS_Store | Bin 0 -> 6148 bytes .../Content/Colorbook UI/Prefabs/UI/.DS_Store | Bin 0 -> 6148 bytes .../Prefabs/UI/SaveGalleryOverlay.prefab | 1079 ++++++++ .../Prefabs/UI/SaveGalleryOverlay.prefab.meta | 7 + .../Fredoka-Bold SDF Blue Outline.asset | 804 +++++- .../Fonts/static/Fredoka-Bold SDF.asset | 226 +- .../Fonts/static/Fredoka-SemiBold SDF.asset | 2300 +++++++++++++++-- .../Darkmatter/Data/Ads/AdUnitCatalog.asset | 3 + Assets/Darkmatter/Scenes/Boot.unity | 1193 +++++++++ .../Android/Facebook.Unity.Android.dll.meta | 17 +- .../Canvas/CanvasJSSDKBindings.jslib.meta | 26 +- .../Canvas/Facebook.Unity.Canvas.dll.meta | 17 +- .../Windows/Facebook.Unity.Windows.dll.meta | 21 +- .../Plugins/Windows/LibFBGManaged.dll.meta | 21 +- .../Plugins/Windows/XInputDotNetPure.dll.meta | 83 +- .../Windows/x64/LibFBGPlatform.dll.meta | 21 +- .../Plugins/Windows/x64/LibFBGUI.dll.meta | 21 +- .../Windows/x64/WebView2Loader.dll.meta | 21 +- .../Windows/x64/XInputInterface.dll.meta | 79 +- .../Plugins/Windows/x64/cpprest_2_10.dll.meta | 21 +- .../Windows/x64/libcrypto-3-x64.dll.meta | 21 +- .../Plugins/Windows/x64/libcurl.dll.meta | 21 +- .../Plugins/Windows/x64/libssl-3-x64.dll.meta | 21 +- .../Plugins/Windows/x64/tinyxml2.dll.meta | 21 +- .../Plugins/Windows/x64/zlib1.dll.meta | 21 +- .../Windows/x86/LibFBGPlatform.dll.meta | 18 +- .../Plugins/Windows/x86/LibFBGUI.dll.meta | 18 +- .../Windows/x86/WebView2Loader.dll.meta | 18 +- .../Windows/x86/XInputInterface.dll.meta | 18 +- .../Plugins/Windows/x86/cpprest_2_10.dll.meta | 18 +- .../Plugins/Windows/x86/libcrypto-3.dll.meta | 18 +- .../Plugins/Windows/x86/libcurl.dll.meta | 18 +- .../Plugins/Windows/x86/libssl-3.dll.meta | 18 +- .../Plugins/Windows/x86/tinyxml2.dll.meta | 18 +- .../Plugins/Windows/x86/zlib1.dll.meta | 18 +- .../Plugins/iOS/Facebook.Unity.IOS.dll.meta | 44 +- .../FBSDKShareTournamentDialog.swift.meta | 26 +- .../iOS/Swift/FBSDKTournament.swift.meta | 26 +- .../Swift/FBSDKTournamentFetcher.swift.meta | 64 +- .../Swift/FBSDKTournamentUpdater.swift.meta | 26 +- Packages/manifest.json | 2 + Packages/packages-lock.json | 50 +- ProjectSettings/EditorBuildSettings.asset | 1 + .../com.unity.ai.assistant/Settings.json | 8 + ProjectSettings/ProjectSettings.asset | 3 +- 69 files changed, 6201 insertions(+), 789 deletions(-) create mode 100644 Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate.meta create mode 100644 Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate/IRewardedSaveGate.cs create mode 100644 Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate/IRewardedSaveGate.cs.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Features.SaveGate.asmdef create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Features.SaveGate.asmdef.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Installers.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Installers/SaveGateModule.cs create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Installers/SaveGateModule.cs.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/SETUP.md create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/SETUP.md.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Systems.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Systems/RewardedSaveGate.cs create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Systems/RewardedSaveGate.cs.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Systems/SaveGateConfig.cs create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/Systems/SaveGateConfig.cs.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/UI.meta create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/UI/SaveGalleryOverlayView.cs create mode 100644 Assets/Darkmatter/Code/Features/SaveGate/UI/SaveGalleryOverlayView.cs.meta create mode 100644 Assets/Darkmatter/Content/Colorbook UI/Prefabs/.DS_Store create mode 100644 Assets/Darkmatter/Content/Colorbook UI/Prefabs/UI/.DS_Store create mode 100644 Assets/Darkmatter/Content/Colorbook UI/Prefabs/UI/SaveGalleryOverlay.prefab create mode 100644 Assets/Darkmatter/Content/Colorbook UI/Prefabs/UI/SaveGalleryOverlay.prefab.meta create mode 100644 ProjectSettings/Packages/com.unity.ai.assistant/Settings.json diff --git a/Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate.meta b/Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate.meta new file mode 100644 index 0000000..e8f173a --- /dev/null +++ b/Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 224a4c0e39279476aa91487a2572bbdd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate/IRewardedSaveGate.cs b/Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate/IRewardedSaveGate.cs new file mode 100644 index 0000000..9ea579e --- /dev/null +++ b/Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate/IRewardedSaveGate.cs @@ -0,0 +1,23 @@ +using System.Threading; +using Cysharp.Threading.Tasks; + +namespace Darkmatter.Core.Contracts.Features.SaveGate +{ + /// + /// Cross-scene gate in front of "save to gallery": shows a kid-friendly prompt and, on confirm, + /// a rewarded ad. Root-scoped so any feature in any scene (capture, art book, …) can reuse it. + /// + public interface IRewardedSaveGate + { + /// + /// Shows the prompt and (on Watch) a rewarded ad. Returns true if the caller should save. + /// + UniTask RequestSaveAsync(CancellationToken cancellationToken = default); + + /// + /// Briefly shows the shared "Saved to gallery!" confirmation. For callers without their own + /// success popup (e.g. the art book). No-op if the overlay has no success panel wired. + /// + UniTask ShowSavedAsync(CancellationToken cancellationToken = default); + } +} diff --git a/Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate/IRewardedSaveGate.cs.meta b/Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate/IRewardedSaveGate.cs.meta new file mode 100644 index 0000000..668b93f --- /dev/null +++ b/Assets/Darkmatter/Code/Core/Contracts/Features/SaveGate/IRewardedSaveGate.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c8ca87e84abf64a0eb0c32de824318b6 \ No newline at end of file diff --git a/Assets/Darkmatter/Code/Features/ArtBook/UI/ArtbookPresenter.cs b/Assets/Darkmatter/Code/Features/ArtBook/UI/ArtbookPresenter.cs index 5c91ee6..41fe0c7 100644 --- a/Assets/Darkmatter/Code/Features/ArtBook/UI/ArtbookPresenter.cs +++ b/Assets/Darkmatter/Code/Features/ArtBook/UI/ArtbookPresenter.cs @@ -5,6 +5,7 @@ using Cysharp.Threading.Tasks; using Darkmatter.Core; using Darkmatter.Core.Contracts.Features.DrawingCatalog; using Darkmatter.Core.Contracts.Features.Progression; +using Darkmatter.Core.Contracts.Features.SaveGate; using Darkmatter.Core.Contracts.Services.Gallery; using Darkmatter.Core.Data.Signals.Features.Drawing; using Darkmatter.Libs.Observer; @@ -22,6 +23,7 @@ namespace Darkmatter.Features.Artbook private readonly IProgressionSystem _progression; private readonly IDrawingTemplateCatalog _catalog; private readonly IGalleryService _gallery; + private readonly IRewardedSaveGate _saveGate; private readonly List _entries = new(); private readonly List _ownedSprites = new(); @@ -36,13 +38,15 @@ namespace Darkmatter.Features.Artbook IEventBus eventBus, IProgressionSystem progression, IDrawingTemplateCatalog catalog, - IGalleryService gallery) + IGalleryService gallery, + IRewardedSaveGate saveGate) { _view = view; _eventBus = eventBus; _progression = progression; _catalog = catalog; _gallery = gallery; + _saveGate = saveGate; } public void Start() @@ -151,7 +155,11 @@ namespace Darkmatter.Features.Artbook { if (!entry.HasValue || entry.Value.Thumbnail == null) return; var ct = _cts?.Token ?? CancellationToken.None; + // Same kid-friendly prompt + rewarded ad as the gameplay save button. + if (!await _saveGate.RequestSaveAsync(ct)) return; await _gallery.SaveImageAsync(entry.Value.Thumbnail, entry.Value.Name, ct); + // The art book has no success popup of its own, so use the shared toast. + await _saveGate.ShowSavedAsync(ct); } private void HandleLeftEditClicked() => OpenForEdit(GetLeftEntry()); diff --git a/Assets/Darkmatter/Code/Features/Capture/Installers/CaptureFeatureModule.cs b/Assets/Darkmatter/Code/Features/Capture/Installers/CaptureFeatureModule.cs index b2bb147..28bbf3e 100644 --- a/Assets/Darkmatter/Code/Features/Capture/Installers/CaptureFeatureModule.cs +++ b/Assets/Darkmatter/Code/Features/Capture/Installers/CaptureFeatureModule.cs @@ -18,6 +18,7 @@ namespace Darkmatter.Features.Capture builder.RegisterInstance(new CaptureConfig(captureScale)); builder.Register(Lifetime.Singleton); + // IRewardedSaveGate is resolved from the root scope (SaveGateModule in Boot). if (captureButtonView != null) builder.RegisterEntryPoint().WithParameter(captureButtonView); diff --git a/Assets/Darkmatter/Code/Features/Capture/UI/CaptureButtonPresenter.cs b/Assets/Darkmatter/Code/Features/Capture/UI/CaptureButtonPresenter.cs index 448a7c2..9ae5842 100644 --- a/Assets/Darkmatter/Code/Features/Capture/UI/CaptureButtonPresenter.cs +++ b/Assets/Darkmatter/Code/Features/Capture/UI/CaptureButtonPresenter.cs @@ -1,6 +1,8 @@ using System; +using System.Threading; using Cysharp.Threading.Tasks; using Darkmatter.Core.Contracts.Features.Capture; +using Darkmatter.Core.Contracts.Features.SaveGate; using VContainer.Unity; namespace Darkmatter.Features.Capture.UI @@ -9,15 +11,20 @@ namespace Darkmatter.Features.Capture.UI { private readonly CaptureButtonView _view; private readonly ICaptureFeature _capture; + private readonly IRewardedSaveGate _saveGate; - public CaptureButtonPresenter(CaptureButtonView view, ICaptureFeature capture) + private CancellationTokenSource _cts; + + public CaptureButtonPresenter(CaptureButtonView view, ICaptureFeature capture, IRewardedSaveGate saveGate) { _view = view; _capture = capture; + _saveGate = saveGate; } public void Start() { + _cts = new CancellationTokenSource(); _view.OnCaptureClicked += HandleCaptureClicked; } @@ -25,11 +32,16 @@ namespace Darkmatter.Features.Capture.UI private async UniTaskVoid CaptureAsync() { + var ct = _cts?.Token ?? CancellationToken.None; _view.SetInteractable(false); try { - await _capture.CapturePngAsync(saveToGallery: true); + // Kid-friendly prompt + rewarded ad (shared, cross-scene gate). The save then + // publishes the existing signals that drive the "Saved to gallery!" popup. + if (await _saveGate.RequestSaveAsync(ct)) + await _capture.CapturePngAsync(saveToGallery: true, ct); } + catch (OperationCanceledException) { } finally { _view.SetInteractable(true); @@ -39,6 +51,9 @@ namespace Darkmatter.Features.Capture.UI public void Dispose() { _view.OnCaptureClicked -= HandleCaptureClicked; + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; } } } diff --git a/Assets/Darkmatter/Code/Features/Coloring/UI/CompletionAnimationView.cs b/Assets/Darkmatter/Code/Features/Coloring/UI/CompletionAnimationView.cs index d6fae2b..81b2a2d 100644 --- a/Assets/Darkmatter/Code/Features/Coloring/UI/CompletionAnimationView.cs +++ b/Assets/Darkmatter/Code/Features/Coloring/UI/CompletionAnimationView.cs @@ -25,10 +25,16 @@ namespace Darkmatter.Features.Coloring.UI rt.localScale = startScale; rt.localRotation = Quaternion.identity; await Tween.Scale(rt, endScale, duration, ease).ToUniTask(cancellationToken: ct); + // The view can be destroyed mid-animation (flow advances, Back/Clear, scene teardown). + // PrimeTween auto-stops the tween on target death and ToUniTask resolves normally, so + // re-check the (Unity fake-null) transform before touching it again — otherwise the + // localRotation write below hits a freed native object and throws NullReferenceException. + if (rt == null) return; await Tween.LocalRotation(rt, new Vector3(0f, 0f, wiggleAngle), wiggleDuration, Ease.InOutSine, cycles: wiggleCycles * 2, cycleMode: CycleMode.Yoyo) .ToUniTask(cancellationToken: ct); + if (rt == null) return; rt.localRotation = Quaternion.identity; } diff --git a/Assets/Darkmatter/Code/Features/SaveGate.meta b/Assets/Darkmatter/Code/Features/SaveGate.meta new file mode 100644 index 0000000..d2ca4ba --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e6434fcc29f4e4949be4be8115fda2b9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Features.SaveGate.asmdef b/Assets/Darkmatter/Code/Features/SaveGate/Features.SaveGate.asmdef new file mode 100644 index 0000000..5102320 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Features.SaveGate.asmdef @@ -0,0 +1,20 @@ +{ + "name": "Features.SaveGate", + "rootNamespace": "Darkmatter.Features.SaveGate", + "references": [ + "GUID:6a0a834eb41764f12ba55c3fb04a40cb", + "GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1", + "GUID:b0214a6008ed146ff8f122a6a9c2f6cc", + "GUID:f51ebe6a0ceec4240a699833d6309b23", + "GUID:6055be8ebefd69e48b49212b09b47b2f" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Features.SaveGate.asmdef.meta b/Assets/Darkmatter/Code/Features/SaveGate/Features.SaveGate.asmdef.meta new file mode 100644 index 0000000..2acb0e3 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Features.SaveGate.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 48cf1e0dac50c4952b2f27e17e8a065a +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Installers.meta b/Assets/Darkmatter/Code/Features/SaveGate/Installers.meta new file mode 100644 index 0000000..c0ced1a --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Installers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8f38ec6a6d3544172b3fdf20e8d31800 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Installers/SaveGateModule.cs b/Assets/Darkmatter/Code/Features/SaveGate/Installers/SaveGateModule.cs new file mode 100644 index 0000000..99747a7 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Installers/SaveGateModule.cs @@ -0,0 +1,31 @@ +using Darkmatter.Features.SaveGate.Systems; +using Darkmatter.Features.SaveGate.UI; +using Darkmatter.Libs.Installers; +using UnityEngine; +using VContainer; +using VContainer.Unity; + +namespace Darkmatter.Features.SaveGate +{ + /// + /// Root module (add to Boot's RootLifetimeScope service modules) so the gate + overlay are + /// resolvable and persistent across every scene. If no overlay is assigned it spawns a + /// self-building one at runtime — no scene/prefab setup required. + /// + public class SaveGateModule : MonoBehaviour, IModule + { + [SerializeField] private SaveGalleryOverlayView overlayView; + [SerializeField, Tooltip("Save anyway when the rewarded ad has no fill or fails to show.")] + private bool saveWithoutAdOnFailure = true; + + public void Register(IContainerBuilder builder) + { + + // Explicit type so a null overlay still resolves; the gate then lets saving proceed + // without a prompt. + builder.RegisterEntryPoint() + .WithParameter(typeof(SaveGalleryOverlayView), overlayView) + .WithParameter(new SaveGateConfig(saveWithoutAdOnFailure)); + } + } +} diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Installers/SaveGateModule.cs.meta b/Assets/Darkmatter/Code/Features/SaveGate/Installers/SaveGateModule.cs.meta new file mode 100644 index 0000000..8a541b2 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Installers/SaveGateModule.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 46ff2e3f5fac64fc9a40fe00a37bd9c8 \ No newline at end of file diff --git a/Assets/Darkmatter/Code/Features/SaveGate/SETUP.md b/Assets/Darkmatter/Code/Features/SaveGate/SETUP.md new file mode 100644 index 0000000..6c691ee --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/SETUP.md @@ -0,0 +1,61 @@ +# SaveGate — cross-scene rewarded-ad save gate (manual Unity wiring) + +Code is complete and compiles. The gate does **not** show its prompt until the overlay is built in +**Boot** and assigned to `SaveGateModule`. Until then both callers fall back to saving directly +(no prompt), so nothing breaks pre-wiring. + +## What it does + +A single root-scoped gate (`IRewardedSaveGate` → `RewardedSaveGate`) used by **every** save-to-gallery +button, in any scene: + +1. Tap save → **kid-friendly prompt** ("Watch a short video to save your picture to the gallery!") + with **Watch** / **Cancel**. +2. **Cancel** → nothing saved. +3. **Watch** → **rewarded ad** (`AdFormat.Rewarded`). Reward earned → caller saves. +4. Ad closed early → not saved. No fill / load-or-show error → saved anyway iff + **Save Without Ad On Failure** (default ON). + +Callers today: +- **Gameplay capture/save button** (`CaptureButtonPresenter`) — gate, then its existing + **GallerySaveView** "Saved to gallery!" popup (unchanged). +- **Art book** page save buttons (`ArtbookPresenter`) — gate, then the gate's **shared success toast** + (the art book has no popup of its own). + +It lives in **Boot** (never unloaded), so the same overlay shows over MainMenu / Colorbook / Gameplay +— the loading-screen trick. + +## The overlay prefab (ready-made) + +A complete, wired overlay prefab already exists: + +`Assets/Darkmatter/Content/Colorbook UI/Prefabs/UI/SaveGalleryOverlay.prefab` + +It has its own screen-space Canvas (sort order 5100, 1080×1920 scaler — matches `TutorialOverlayCanvas`), +the `SaveGalleryOverlayView` on the root with all six fields wired, and: +- **PromptPanel** → dim backdrop + cream Card → Fredoka message, green **Watch video**, grey **Not now**. +- **SuccessPanel** (starts inactive) → dark toast → "Saved to gallery!". + +Edit the copy/colors freely; it uses plain colored rects (no sprite art) so a designer can drop in +sprites later. To wire it: just drag the prefab into **Boot** (it's a self-contained Canvas, so it +works as-is) and assign it to `SaveGateModule` below. Boot is never unloaded, so it persists across +every scene. (Building your own overlay also works — put `SaveGalleryOverlayView` on an always-active +object and wire promptPanel / watchButton / cancelButton / promptLabel / successPanel / successLabel.) + +## Register in DI (Boot RootLifetimeScope) + +1. Add a **`SaveGateModule`** component in Boot (it's an `IModule`). +2. Add it to the **RootLifetimeScope**'s `serviceModules` list (same list that holds the Ad / Loading + modules) so it registers at the root. +3. On `SaveGateModule`: assign **Overlay View** → the `SaveGalleryOverlayView`; tick + **Save Without Ad On Failure** (recommended). + +`IAdService` is already root-scoped (AdServiceModule in Boot); the gate resolves it automatically, and +`CaptureButtonPresenter` / `ArtbookPresenter` resolve `IRewardedSaveGate` from the root. + +## Before release + +- `AdUnitCatalogSO` holds AdMob **test** unit IDs (incl. test Rewarded). Swap in the real Rewarded ID. +- Child-directed / COPPA app (IDFA off, `TagForChildDirectedTreatment.True`, `MaxAdContentRating.G` + already set). Confirm rewarded ads are permitted under the relevant kids / family policies before + shipping rewarded-gated saving. diff --git a/Assets/Darkmatter/Code/Features/SaveGate/SETUP.md.meta b/Assets/Darkmatter/Code/Features/SaveGate/SETUP.md.meta new file mode 100644 index 0000000..1de7019 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/SETUP.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9db44e977bbde446daa19aa928ee1230 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Systems.meta b/Assets/Darkmatter/Code/Features/SaveGate/Systems.meta new file mode 100644 index 0000000..b63b4a1 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Systems.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 010c08604fc7e44ceb9d069874618ce8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Systems/RewardedSaveGate.cs b/Assets/Darkmatter/Code/Features/SaveGate/Systems/RewardedSaveGate.cs new file mode 100644 index 0000000..f0bd6d8 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Systems/RewardedSaveGate.cs @@ -0,0 +1,145 @@ +using System; +using System.Threading; +using Cysharp.Threading.Tasks; +using Darkmatter.Core.Contracts.Features.SaveGate; +using Darkmatter.Core.Contracts.Services.Ads; +using Darkmatter.Core.Enums.Services.Ads; +using Darkmatter.Features.SaveGate.UI; +using VContainer.Unity; + +namespace Darkmatter.Features.SaveGate.Systems +{ + public class RewardedSaveGate : IRewardedSaveGate, IStartable, IDisposable + { + private readonly SaveGalleryOverlayView _overlay; + private readonly IAdService _ads; + private readonly SaveGateConfig _config; + + private CancellationTokenSource _cts; + private bool _busy; + + public RewardedSaveGate(SaveGalleryOverlayView overlay, IAdService ads, SaveGateConfig config) + { + _overlay = overlay; + _ads = ads; + _config = config; + } + + public void Start() + { + _cts = new CancellationTokenSource(); + // Warm a rewarded ad so the gate is snappy on first save. + PrewarmRewardedAsync(_cts.Token).Forget(); + } + + public async UniTask RequestSaveAsync(CancellationToken cancellationToken = default) + { + // Overlay not built yet, or no ad service: never block saving. + if (_overlay == null || _ads == null) return true; + if (_busy) return false; // a prompt is already up; ignore the extra tap + + _busy = true; + using var linked = CancellationTokenSource.CreateLinkedTokenSource( + _cts?.Token ?? CancellationToken.None, cancellationToken); + var ct = linked.Token; + try + { + if (!await ShowPromptAsync(ct)) return false; // child tapped Cancel + return await TryShowRewardedAsync(ct); // ad closed early / unavailable + } + catch (OperationCanceledException) { return false; } + finally + { + _busy = false; + } + } + + public async UniTask ShowSavedAsync(CancellationToken cancellationToken = default) + { + if (_overlay == null || !_overlay.HasSuccessPanel) return; + using var linked = CancellationTokenSource.CreateLinkedTokenSource( + _cts?.Token ?? CancellationToken.None, cancellationToken); + _overlay.ShowSuccess(); + try + { + await UniTask.Delay(TimeSpan.FromSeconds(_overlay.SuccessAutoHideSeconds), + cancellationToken: linked.Token); + } + catch (OperationCanceledException) { } + _overlay.HideSuccess(); + } + + // Shows the prompt and resolves true on Watch, false on Cancel (or cancellation). + private UniTask ShowPromptAsync(CancellationToken ct) + { + var tcs = new UniTaskCompletionSource(); + CancellationTokenRegistration reg = default; + var done = false; + + void Finish(bool result, bool canceled) + { + if (done) return; + done = true; + _overlay.OnWatch -= OnWatch; + _overlay.OnCancel -= OnCancel; + reg.Dispose(); + _overlay.HidePrompt(); + if (canceled) tcs.TrySetCanceled(ct); + else tcs.TrySetResult(result); + } + + void OnWatch() => Finish(true, false); + void OnCancel() => Finish(false, false); + + _overlay.OnWatch += OnWatch; + _overlay.OnCancel += OnCancel; + _overlay.ShowPrompt(); + reg = ct.Register(() => Finish(false, true)); + return tcs.Task; + } + + // Returns true only when the child earned the reward. Falls back to SaveWithoutAdOnFailure + // when the ad can't be shown at all; an ad closed early counts as not rewarded -> no save. + private async UniTask TryShowRewardedAsync(CancellationToken ct) + { + try + { + if (!_ads.IsInitialized) await _ads.InitializeAsync(ct); + if (!_ads.IsReady(AdFormat.Rewarded) && !await _ads.LoadAsync(AdFormat.Rewarded, ct)) + return _config.SaveWithoutAdOnFailure; + + var result = await _ads.ShowAsync(AdFormat.Rewarded, ct); + if (result.Rewarded) return true; + return result.Shown ? false : _config.SaveWithoutAdOnFailure; + } + catch (OperationCanceledException) { throw; } + catch (Exception ex) + { + UnityEngine.Debug.LogWarning($"[SaveGate] Rewarded ad gate failed: {ex.Message}"); + return _config.SaveWithoutAdOnFailure; + } + } + + private async UniTaskVoid PrewarmRewardedAsync(CancellationToken ct) + { + if (_ads == null) return; + try + { + if (!_ads.IsInitialized) await _ads.InitializeAsync(ct); + if (!_ads.IsReady(AdFormat.Rewarded)) await _ads.LoadAsync(AdFormat.Rewarded, ct); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + UnityEngine.Debug.LogWarning($"[SaveGate] Rewarded prewarm failed: {ex.Message}"); + } + } + + public void Dispose() + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + } + } +} diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Systems/RewardedSaveGate.cs.meta b/Assets/Darkmatter/Code/Features/SaveGate/Systems/RewardedSaveGate.cs.meta new file mode 100644 index 0000000..3dcf73a --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Systems/RewardedSaveGate.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bcff7c06c2d77499db81865208f807e7 \ No newline at end of file diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Systems/SaveGateConfig.cs b/Assets/Darkmatter/Code/Features/SaveGate/Systems/SaveGateConfig.cs new file mode 100644 index 0000000..d572a98 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Systems/SaveGateConfig.cs @@ -0,0 +1,18 @@ +using System; + +namespace Darkmatter.Features.SaveGate +{ + [Serializable] + public struct SaveGateConfig + { + // When the rewarded ad can't be shown (no fill / load or show error), save anyway instead of + // blocking the child. A user who opens the ad and closes it early is never rewarded and does + // not trigger this fallback. + public bool SaveWithoutAdOnFailure { get; } + + public SaveGateConfig(bool saveWithoutAdOnFailure) + { + SaveWithoutAdOnFailure = saveWithoutAdOnFailure; + } + } +} diff --git a/Assets/Darkmatter/Code/Features/SaveGate/Systems/SaveGateConfig.cs.meta b/Assets/Darkmatter/Code/Features/SaveGate/Systems/SaveGateConfig.cs.meta new file mode 100644 index 0000000..4e73294 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/Systems/SaveGateConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c7343ebeefe9e4db48c41c617347adae \ No newline at end of file diff --git a/Assets/Darkmatter/Code/Features/SaveGate/UI.meta b/Assets/Darkmatter/Code/Features/SaveGate/UI.meta new file mode 100644 index 0000000..6094825 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1356ea11d3edf4c1293bc8dec5ecdb3a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Darkmatter/Code/Features/SaveGate/UI/SaveGalleryOverlayView.cs b/Assets/Darkmatter/Code/Features/SaveGate/UI/SaveGalleryOverlayView.cs new file mode 100644 index 0000000..6844ab7 --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/UI/SaveGalleryOverlayView.cs @@ -0,0 +1,77 @@ +using System; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Darkmatter.Features.SaveGate.UI +{ + /// + /// Root-scoped, cross-scene overlay for the save-to-gallery flow: a kid-friendly "watch a video" + /// prompt and a "Saved!" toast. Dumb view — it shows/hides panels and raises button events; + /// RewardedSaveGate drives the flow. Lives in Boot so it persists over every scene. + /// + /// If the panels aren't wired in the inspector it builds the whole UI in code (see + /// ), so the feature works with no manual prefab setup. A designer + /// can still wire custom panels and the code build is skipped. + /// + public class SaveGalleryOverlayView : MonoBehaviour + { + [Header("Watch-ad prompt")] [SerializeField] + private GameObject promptPanel; + + [SerializeField] private Button watchButton; + [SerializeField] private Button cancelButton; + [SerializeField] private TMP_Text promptLabel; + + [Header("Saved toast")] [SerializeField] + private GameObject successPanel; + + [SerializeField] private TMP_Text successLabel; + [SerializeField, Min(0f)] private float successAutoHideSeconds = 1.5f; + + public event Action OnWatch; + public event Action OnCancel; + + public float SuccessAutoHideSeconds => successAutoHideSeconds; + public bool HasSuccessPanel => successPanel != null; + + private void Awake() + { + if (promptPanel != null) promptPanel.SetActive(false); + if (successPanel != null) successPanel.SetActive(false); + } + + private void Start() + { + if (watchButton != null) watchButton.onClick.AddListener(() => OnWatch?.Invoke()); + if (cancelButton != null) cancelButton.onClick.AddListener(() => OnCancel?.Invoke()); + } + + public void ShowPrompt() + { + if (successPanel != null) successPanel.SetActive(false); + if (promptPanel != null) promptPanel.SetActive(true); + } + + public void HidePrompt() + { + if (promptPanel != null) promptPanel.SetActive(false); + } + + public void ShowSuccess() + { + if (successPanel != null) successPanel.SetActive(true); + } + + public void HideSuccess() + { + if (successPanel != null) successPanel.SetActive(false); + } + + private void OnDestroy() + { + if (watchButton != null) watchButton.onClick.RemoveAllListeners(); + if (cancelButton != null) cancelButton.onClick.RemoveAllListeners(); + } + } +} diff --git a/Assets/Darkmatter/Code/Features/SaveGate/UI/SaveGalleryOverlayView.cs.meta b/Assets/Darkmatter/Code/Features/SaveGate/UI/SaveGalleryOverlayView.cs.meta new file mode 100644 index 0000000..a6314dc --- /dev/null +++ b/Assets/Darkmatter/Code/Features/SaveGate/UI/SaveGalleryOverlayView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e2688ce9b9d9848719e2ee33cf6eb155 \ No newline at end of file diff --git a/Assets/Darkmatter/Code/Features/Tutorial/Systems/TutorialDirector.cs b/Assets/Darkmatter/Code/Features/Tutorial/Systems/TutorialDirector.cs index df17570..bedad24 100644 --- a/Assets/Darkmatter/Code/Features/Tutorial/Systems/TutorialDirector.cs +++ b/Assets/Darkmatter/Code/Features/Tutorial/Systems/TutorialDirector.cs @@ -46,6 +46,7 @@ namespace Darkmatter.Features.Tutorial.Systems private CancellationToken _ct; private bool _completed; private bool _drawingCompleted; + private bool _hasColored; private bool _suspended; private Action _reshow; private int _stepIndex; @@ -80,6 +81,9 @@ namespace Darkmatter.Features.Tutorial.Systems // finds Next early during free-paint. Flag it so the catalog reload that follows isn't // mistaken for a Back-navigation and doesn't restart the tutorial. _navSubs.Add(_bus.Subscribe(_ => _drawingCompleted = true)); + // Painting at least one region means the child has grasped the core colour gesture. If + // they then go Back, we finish the tutorial instead of restarting it (OnCatalogReadyGlobal). + _navSubs.Add(_bus.Subscribe(_ => _hasColored = true)); StartRun(skipCatalogWait: false); } @@ -93,6 +97,7 @@ namespace Darkmatter.Features.Tutorial.Systems _gen++; _suspended = false; _drawingCompleted = false; + _hasColored = false; _reshow = null; _stepIndex = 0; _overlay.HideInstant(); @@ -119,8 +124,19 @@ namespace Darkmatter.Features.Tutorial.Systems // press), whose own completion loads the catalog. private void OnCatalogReadyGlobal(DrawingCatalogReadySignal _) { - if (!_completed && !_drawingCompleted && _stepIndex >= 2 && _stepIndex <= 6) - StartRun(skipCatalogWait: true); + if (_completed || _drawingCompleted || _stepIndex < 2 || _stepIndex > 6) return; + + // Already coloured at least one region before backing out: the child has the core gesture, + // so finish the tutorial rather than trapping them in a full restart. + if (_hasColored) + { + _runCts?.Cancel(); + Complete(); + _overlay.HideInstant(); + return; + } + + StartRun(skipCatalogWait: true); } private void ShowStep(Action show) diff --git a/Assets/Darkmatter/Content/Colorbook UI/Prefabs/.DS_Store b/Assets/Darkmatter/Content/Colorbook UI/Prefabs/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..77d925a320ba3d3465a6a07d3d13d822bd244fc0 GIT binary patch literal 6148 zcmeH~u?oUK42F~1L2&8lc#99<8yr#x=i=@lxJU&-=X-SjuF3?CYIR-0mATH8ev5}0NO0~MeG69pDE@9g~F!@te{lNP2_fC~JX z0^0UNzr#z#+4}K%RzF46)&&mwZ3|Uq60r=p(ufg5wz~*s6pGs_FRBe&R^k)72q$*?`2AuAiJU6N}L zm-acz6YW^_##O7**QJgPJkH8des^X)q2R%9d9yMX9miIntV?PLNhLu=t7OZVi*@rf8ghZ9%F|t z9LBCp9>22jHxy%6r$4aMVM2#ms{*RPxB@G#Ta@$vB>Vn99;7{0KowXj1x&tHukm8V zLwCB#SsSoivxv#O*r5$!<&R_AkfXT7VvS>gG>D